diff --git a/AUTHORS b/AUTHORS index 5def3f5e99..18fe51397e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,4 +11,6 @@ Jacob Alzén Charles A. Daniels Pablo Fuentes Changkun Ou +Cedric Bail +Drew Weymouth diff --git a/app/app.go b/app/app.go index fa93fca969..3759c95294 100644 --- a/app/app.go +++ b/app/app.go @@ -64,6 +64,7 @@ func (a *fyneApp) NewWindow(title string) fyne.Window { } func (a *fyneApp) Run() { + go a.lifecycle.RunEventQueue() a.driver.Run() } @@ -138,6 +139,7 @@ func newAppWithDriver(d fyne.Driver, id string) fyne.App { fyne.SetCurrentApp(newApp) newApp.prefs = newApp.newDefaultPreferences() + newApp.lifecycle.InitEventQueue() newApp.lifecycle.SetOnStoppedHookExecuted(func() { if prefs, ok := newApp.prefs.(*preferences); ok { prefs.forceImmediateSave() diff --git a/app/app_desktop_darwin.go b/app/app_desktop_darwin.go index 2142503299..1390a14dd0 100644 --- a/app/app_desktop_darwin.go +++ b/app/app_desktop_darwin.go @@ -9,7 +9,6 @@ package app #include bool isBundled(); -bool isDarkMode(); void watchTheme(); */ import "C" @@ -20,15 +19,12 @@ import ( "path/filepath" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" ) -// SetSystemTrayMenu creates a system tray item and attaches the specified menu. -// By default this will use the application icon. -func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) { - if desk, ok := a.Driver().(systrayDriver); ok { - desk.SetSystemTrayMenu(menu) - } +func (a *fyneApp) OpenURL(url *url.URL) error { + cmd := exec.Command("open", url.String()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + return cmd.Run() } // SetSystemTrayIcon sets a custom image for the system tray icon. @@ -37,11 +33,12 @@ func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) { a.Driver().(systrayDriver).SetSystemTrayIcon(icon) } -func defaultVariant() fyne.ThemeVariant { - if C.isDarkMode() { - return theme.VariantDark +// SetSystemTrayMenu creates a system tray item and attaches the specified menu. +// By default this will use the application icon. +func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) { + if desk, ok := a.Driver().(systrayDriver); ok { + desk.SetSystemTrayMenu(menu) } - return theme.VariantLight } func rootConfigDir() string { @@ -51,12 +48,6 @@ func rootConfigDir() string { return filepath.Join(desktopConfig, "fyne") } -func (a *fyneApp) OpenURL(url *url.URL) error { - cmd := exec.Command("open", url.String()) - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - return cmd.Run() -} - //export themeChanged func themeChanged() { fyne.CurrentApp().Settings().(*settings).setupTheme() diff --git a/app/app_desktop_darwin.m b/app/app_desktop_darwin.m index a571f6642a..3aa12910a6 100644 --- a/app/app_desktop_darwin.m +++ b/app/app_desktop_darwin.m @@ -4,11 +4,6 @@ #import -bool isDarkMode() { - NSString *style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; - return [@"Dark" isEqualToString:style]; -} - void watchTheme() { [[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"AppleInterfaceThemeChangedNotification" object:nil queue:nil usingBlock:^(NSNotification *note) { diff --git a/app/app_mobile.go b/app/app_mobile.go index 9976bb0d88..0303c202a2 100644 --- a/app/app_mobile.go +++ b/app/app_mobile.go @@ -4,18 +4,17 @@ package app import ( "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/driver/mobile" ) -var systemTheme fyne.ThemeVariant - // NewWithID returns a new app instance using the appropriate runtime driver. // The ID string should be globally unique to this app. func NewWithID(id string) fyne.App { d := mobile.NewGoMobileDriver() a := newAppWithDriver(d, id) d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) { - systemTheme = c.SystemTheme + internalapp.SystemTheme = c.SystemTheme a.Settings().(*settings).setupTheme() }) diff --git a/app/app_mobile_and.go b/app/app_mobile_and.go index 35baf26e14..9abf04745c 100644 --- a/app/app_mobile_and.go +++ b/app/app_mobile_and.go @@ -45,10 +45,6 @@ func (a *fyneApp) SendNotification(n *fyne.Notification) { }) } -func defaultVariant() fyne.ThemeVariant { - return systemTheme -} - func rootConfigDir() string { filesDir := os.Getenv("FILESDIR") if filesDir == "" { diff --git a/app/app_mobile_ios.go b/app/app_mobile_ios.go index fb79323d91..991e4b6ea7 100644 --- a/app/app_mobile_ios.go +++ b/app/app_mobile_ios.go @@ -17,8 +17,6 @@ import ( "net/url" "path/filepath" "unsafe" - - "fyne.io/fyne/v2" ) func rootConfigDir() string { @@ -33,7 +31,3 @@ func (a *fyneApp) OpenURL(url *url.URL) error { return nil } - -func defaultVariant() fyne.ThemeVariant { - return systemTheme -} diff --git a/app/app_other.go b/app/app_other.go index d20799ba7f..b14d4f7040 100644 --- a/app/app_other.go +++ b/app/app_other.go @@ -1,4 +1,4 @@ -//go:build ci || (!linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver) +//go:build ci || (mobile && !android && !ios) || (!linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver) package app @@ -9,13 +9,8 @@ import ( "path/filepath" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" ) -func defaultVariant() fyne.ThemeVariant { - return theme.VariantDark -} - func rootConfigDir() string { return filepath.Join(os.TempDir(), "fyne-test") } diff --git a/app/app_theme_web.go b/app/app_theme_web.go deleted file mode 100644 index df16339216..0000000000 --- a/app/app_theme_web.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !ci && !wasm && test_web_driver - -package app - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" -) - -func defaultVariant() fyne.ThemeVariant { - return theme.VariantDark -} diff --git a/app/app_windows.go b/app/app_windows.go index e2a8f713ed..bc648d9cfe 100644 --- a/app/app_windows.go +++ b/app/app_windows.go @@ -11,10 +11,7 @@ import ( "strings" "syscall" - "golang.org/x/sys/windows/registry" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" ) const notificationTemplate = `$title = "%s" @@ -31,28 +28,6 @@ $xml.LoadXml($toastXml.OuterXml) $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("%s").Show($toast);` -func isDark() bool { - k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) - if err != nil { // older version of Windows will not have this key - return false - } - defer k.Close() - - useLight, _, err := k.GetIntegerValue("AppsUseLightTheme") - if err != nil { // older version of Windows will not have this value - return false - } - - return useLight == 0 -} - -func defaultVariant() fyne.ThemeVariant { - if isDark() { - return theme.VariantDark - } - return theme.VariantLight -} - func rootConfigDir() string { homeDir, _ := os.UserHomeDir() diff --git a/app/app_xdg.go b/app/app_xdg.go index 88ff6ad7ee..2bf9327fbc 100644 --- a/app/app_xdg.go +++ b/app/app_xdg.go @@ -1,4 +1,4 @@ -//go:build !ci && !wasm && !test_web_driver && (linux || openbsd || freebsd || netbsd) && !android +//go:build !ci && !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd) package app @@ -16,16 +16,11 @@ import ( "github.com/rymdport/portal/settings/appearance" "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/theme" ) -var currentVariant atomic.Uint64 - -func defaultVariant() fyne.ThemeVariant { - return fyne.ThemeVariant(currentVariant.Load()) -} - func (a *fyneApp) OpenURL(url *url.URL) error { if build.IsFlatpak { err := openuri.OpenURI("", url.String(), nil) @@ -123,11 +118,11 @@ func rootConfigDir() string { func watchTheme() { go func() { // with portal this may not be immediate, so we update a cache instead - currentVariant.Store(uint64(findFreedesktopColorScheme())) + internalapp.CurrentVariant.Store(uint64(findFreedesktopColorScheme())) portalSettings.OnSignalSettingChanged(func(changed portalSettings.Changed) { if changed.Namespace == "org.freedesktop.appearance" && changed.Key == "color-scheme" { - currentVariant.Store(uint64(findFreedesktopColorScheme())) + internalapp.CurrentVariant.Store(uint64(findFreedesktopColorScheme())) fyne.CurrentApp().Settings().(*settings).setupTheme() } }) diff --git a/app/preferences_test.go b/app/preferences_test.go index 9f0de61e2c..f30df7021f 100644 --- a/app/preferences_test.go +++ b/app/preferences_test.go @@ -72,8 +72,7 @@ func TestPreferences_Save_OverwriteFast(t *testing.T) { val["key"] = "value" }) - path := filepath.Join(os.TempDir(), "fynePrefs2.json") - defer os.Remove(path) + path := filepath.Join(t.TempDir(), "fynePrefs2.json") p.saveToFile(path) p.WriteValues(func(val map[string]any) { diff --git a/app/settings.go b/app/settings.go index 2b6fddd62c..ca83c84e6d 100644 --- a/app/settings.go +++ b/app/settings.go @@ -7,6 +7,7 @@ import ( "sync" "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/theme" ) @@ -152,7 +153,7 @@ func (s *settings) setupTheme() { name = env } - variant := defaultVariant() + variant := internalapp.DefaultVariant() effectiveTheme := s.theme if !s.themeSpecified { effectiveTheme = s.loadSystemTheme() diff --git a/app/settings_test.go b/app/settings_test.go index 65745df0f6..ade718653e 100644 --- a/app/settings_test.go +++ b/app/settings_test.go @@ -6,6 +6,7 @@ import ( "testing" "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/build" internalTest "fyne.io/fyne/v2/internal/test" "fyne.io/fyne/v2/test" @@ -43,7 +44,7 @@ func TestSettingsLoad(t *testing.T) { func TestOverrideTheme(t *testing.T) { set := &settings{} set.setupTheme() - assert.Equal(t, defaultVariant(), set.ThemeVariant()) + assert.Equal(t, internalapp.DefaultVariant(), set.ThemeVariant()) set.schema.ThemeName = "light" set.setupTheme() @@ -57,7 +58,7 @@ func TestOverrideTheme(t *testing.T) { set = &settings{} set.setupTheme() - assert.Equal(t, defaultVariant(), set.ThemeVariant()) + assert.Equal(t, internalapp.DefaultVariant(), set.ThemeVariant()) err := os.Setenv("FYNE_THEME", "light") if err != nil { @@ -106,7 +107,7 @@ func TestCustomTheme(t *testing.T) { set.setupTheme() assert.True(t, set.Theme() == ctheme) - assert.Equal(t, defaultVariant(), set.ThemeVariant()) + assert.Equal(t, internalapp.DefaultVariant(), set.ThemeVariant()) err := set.loadFromFile(filepath.Join("testdata", "light-theme.json")) if err != nil { diff --git a/canvas/image.go b/canvas/image.go index fa2bb95113..f4d512c849 100644 --- a/canvas/image.go +++ b/canvas/image.go @@ -270,7 +270,15 @@ func (i *Image) updateReader() (io.ReadCloser, error) { i.isSVG = false if i.Resource != nil { i.isSVG = svg.IsResourceSVG(i.Resource) - return io.NopCloser(bytes.NewReader(i.Resource.Content())), nil + content := i.Resource.Content() + if res, ok := i.Resource.(fyne.ThemedResource); i.isSVG && ok { + th := cache.WidgetTheme(i) + if th != nil { + col := th.Color(res.ThemeColorName(), fyne.CurrentApp().Settings().ThemeVariant()) + content = svg.Colorize(content, col) + } + } + return io.NopCloser(bytes.NewReader(content)), nil } else if i.File != "" { var err error @@ -347,7 +355,7 @@ func (i *Image) renderSVG(width, height float32) (image.Image, error) { screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height}) } - tex := cache.GetSvg(i.name(), screenWidth, screenHeight) + tex := cache.GetSvg(i.name(), i, screenWidth, screenHeight) if tex != nil { return tex, nil } @@ -357,6 +365,6 @@ func (i *Image) renderSVG(width, height float32) (image.Image, error) { if err != nil { return nil, err } - cache.SetSvg(i.name(), tex, screenWidth, screenHeight) + cache.SetSvg(i.name(), i, tex, screenWidth, screenHeight) return tex, nil } diff --git a/canvas/image_test.go b/canvas/image_test.go index 91c02df42d..6122a9d0a6 100644 --- a/canvas/image_test.go +++ b/canvas/image_test.go @@ -10,9 +10,8 @@ import ( "testing" "fyne.io/fyne/v2/canvas" - intRepo "fyne.io/fyne/v2/internal/repository" "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/storage/repository" + _ "fyne.io/fyne/v2/test" "github.com/stretchr/testify/assert" ) @@ -43,6 +42,7 @@ func TestNewImageFromReader(t *testing.T) { path := filepath.Join(filepath.Dir(pwd), "theme", "icons", "fyne.png") read, err := os.Open(path) assert.Nil(t, err) + defer read.Close() img := canvas.NewImageFromReader(read, "fyne.png") assert.NotNil(t, img) @@ -77,9 +77,6 @@ func TestNewImageFromURI_File(t *testing.T) { } func TestNewImageFromURI_HTTP(t *testing.T) { - h := intRepo.NewHTTPRepository() - repository.Register("http", h) - pwd, _ := os.Getwd() path := filepath.Join(filepath.Dir(pwd), "theme", "icons", "fyne.png") f, _ := os.ReadFile(path) @@ -95,6 +92,7 @@ func TestNewImageFromURI_HTTP(t *testing.T) { })) defer ts.Close() + // http is mounted in Fyne test handlers by default url, _ := storage.ParseURI(ts.URL) img := canvas.NewImageFromURI(url) assert.NotNil(t, img) diff --git a/canvas/text.go b/canvas/text.go index d4415d3bc0..08e8a7f783 100644 --- a/canvas/text.go +++ b/canvas/text.go @@ -21,6 +21,13 @@ type Text struct { Text string // The string content of this Text TextSize float32 // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size TextStyle fyne.TextStyle // The style of the text content + + // FontSource defines a resource that can be used instead of the theme for looking up the font. + // When a font source is set the `TextStyle` may not be effective, as it will be limited to the styles + // present in the data provided. + // + // Since: 2.5 + FontSource fyne.Resource } // Hide will set this text to not be visible @@ -33,7 +40,8 @@ func (t *Text) Hide() { // MinSize returns the minimum size of this text object based on its font size and content. // This is normally determined by the render implementation. func (t *Text) MinSize() fyne.Size { - return fyne.MeasureText(t.Text, t.TextSize, t.TextStyle) + s, _ := fyne.CurrentApp().Driver().RenderedTextSize(t.Text, t.TextSize, t.TextStyle, t.FontSource) + return s } // Move the text to a new position, relative to its parent / canvas diff --git a/canvas/text_test.go b/canvas/text_test.go index bc3ec9c197..5af9e76a59 100644 --- a/canvas/text_test.go +++ b/canvas/text_test.go @@ -1,6 +1,7 @@ package canvas_test import ( + "image" "image/color" "testing" @@ -12,6 +13,22 @@ import ( "github.com/stretchr/testify/assert" ) +func TestText_FontSource(t *testing.T) { + text := canvas.NewText("Test", color.NRGBA{0, 0, 0, 0xff}) + c := test.NewWindow(text).Canvas() + + text.FontSource = test.Theme().Font(fyne.TextStyle{Bold: true}) + img1 := c.Capture() + text.FontSource = test.Theme().Font(fyne.TextStyle{Italic: true}) + img2 := c.Capture() + assert.NotEqual(t, img1.(*image.NRGBA).Pix, img2.(*image.NRGBA).Pix) + + text.FontSource = fyne.NewStaticResource("corrupt", []byte{}) + assert.NotPanics(t, func() { + test.NewWindow(text).Canvas().Capture() + }) +} + func TestText_MinSize(t *testing.T) { text := canvas.NewText("Test", color.NRGBA{0, 0, 0, 0xff}) min := text.MinSize() diff --git a/cmd/fyne_demo/data/metadata_bundled.go b/cmd/fyne_demo/data/metadata_bundled.go index 383d5eb2cf..2805627dff 100644 --- a/cmd/fyne_demo/data/metadata_bundled.go +++ b/cmd/fyne_demo/data/metadata_bundled.go @@ -8,5 +8,5 @@ import "fyne.io/fyne/v2" var resourceAuthors = &fyne.StaticResource{ StaticName: "AUTHORS", StaticContent: []byte( - "Andy Williams \nSteve OConnor \nLuca Corbo \nPaul Hovey \nCharles Corbett \nTilo Prütz \nStephen Houston \nStorm Hess \nStuart Scott \nJacob Alzén \nCharles A. Daniels \nPablo Fuentes \nChangkun Ou \n\n"), + "Andy Williams \nSteve OConnor \nLuca Corbo \nPaul Hovey \nCharles Corbett \nTilo Prütz \nStephen Houston \nStorm Hess \nStuart Scott \nJacob Alzén \nCharles A. Daniels \nPablo Fuentes \nChangkun Ou \nCedric Bail\nDrew Weymouth\n\n"), } diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index 81deb5fcbb..f5c93b3567 100644 --- a/cmd/fyne_demo/main.go +++ b/cmd/fyne_demo/main.go @@ -120,6 +120,12 @@ func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu { w.Resize(fyne.NewSize(440, 520)) w.Show() } + showAbout := func() { + w := a.NewWindow("About") + w.SetContent(widget.NewLabel("About Fyne Demo app...")) + w.Show() + } + aboutItem := fyne.NewMenuItem("About", showAbout) settingsItem := fyne.NewMenuItem("Settings", openSettings) settingsShortcut := &desktop.CustomShortcut{KeyName: fyne.KeyComma, Modifier: fyne.KeyModifierShortcutDefault} settingsItem.Shortcut = settingsShortcut @@ -170,6 +176,7 @@ func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu { if !device.IsMobile() && !device.IsBrowser() { file.Items = append(file.Items, fyne.NewMenuItemSeparator(), settingsItem) } + file.Items = append(file.Items, aboutItem) main := fyne.NewMainMenu( file, fyne.NewMenu("Edit", cutItem, copyItem, pasteItem, fyne.NewMenuItemSeparator(), findItem), diff --git a/cmd/fyne_demo/tutorials/animation.go b/cmd/fyne_demo/tutorials/animation.go index 6e2c71a102..532c9bad2a 100644 --- a/cmd/fyne_demo/tutorials/animation.go +++ b/cmd/fyne_demo/tutorials/animation.go @@ -21,11 +21,14 @@ func makeAnimationCanvas() fyne.CanvasObject { rect := canvas.NewRectangle(color.Black) rect.Resize(fyne.NewSize(410, 140)) - a := canvas.NewColorRGBAAnimation(theme.PrimaryColorNamed(theme.ColorBlue), theme.PrimaryColorNamed(theme.ColorGreen), + a := canvas.NewColorRGBAAnimation( + color.NRGBA{R: 0x29, G: 0x6f, B: 0xf6, A: 0xaa}, + color.NRGBA{R: 0x8b, G: 0xc3, B: 0x4a, A: 0xaa}, time.Second*3, func(c color.Color) { rect.FillColor = c canvas.Refresh(rect) - }) + }, + ) a.RepeatCount = fyne.AnimationRepeatForever a.AutoReverse = true a.Start() diff --git a/cmd/fyne_demo/tutorials/widget.go b/cmd/fyne_demo/tutorials/widget.go index 2eb45b4f30..1654340c60 100644 --- a/cmd/fyne_demo/tutorials/widget.go +++ b/cmd/fyne_demo/tutorials/widget.go @@ -340,7 +340,34 @@ This styled row should also wrap as expected, but only *when required*. } func makeInputTab(_ fyne.Window) fyne.CanvasObject { - selectEntry := widget.NewSelectEntry([]string{"Option A", "Option B", "Option C"}) + selectEntry := widget.NewSelectEntry([]string{ + "Option A", + "Option B", + "Option C", + "Option D", + "Option E", + "Option F", + "Option G", + "Option H", + "Option I", + "Option J", + "Option K", + "Option L", + "Option M", + "Option N", + "Option O", + "Option P", + "Option Q", + "Option R", + "Option S", + "Option T", + "Option U", + "Option V", + "Option W", + "Option X", + "Option Y", + "Option Z", + }) selectEntry.PlaceHolder = "Type or select" disabledCheck := widget.NewCheck("Disabled check", func(bool) {}) disabledCheck.Disable() @@ -470,6 +497,10 @@ func startProgress() { } func stopProgress() { + if infProgress == nil { + return + } + if !infProgress.Running() { return } diff --git a/cmd/fyne_settings/settings/appearance.go b/cmd/fyne_settings/settings/appearance.go index 3f3b024ac7..7c249bbac0 100644 --- a/cmd/fyne_settings/settings/appearance.go +++ b/cmd/fyne_settings/settings/appearance.go @@ -11,6 +11,7 @@ import ( "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + internalapp "fyne.io/fyne/v2/internal/app" intWidget "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" @@ -18,7 +19,10 @@ import ( ) const ( - systemThemeName = "system default" + themeNameDark = "dark" + themeNameLight = "light" + themeNameSystem = "" + themeNameSystemLabel = "system default" ) // Settings gives access to user interfaces to control Fyne settings @@ -56,11 +60,11 @@ func (s *Settings) LoadAppearanceScreen(w fyne.Window) fyne.CanvasObject { s.preview = s.createPreview() def := s.fyneSettings.ThemeName - themeNames := []string{"dark", "light"} + themeNames := []string{themeNameDark, themeNameLight} if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { - themeNames = append(themeNames, systemThemeName) - if s.fyneSettings.ThemeName == "" { - def = systemThemeName + themeNames = append(themeNames, themeNameSystemLabel) + if s.fyneSettings.ThemeName == themeNameSystem { + def = themeNameSystemLabel } } themes := widget.NewSelect(themeNames, s.chooseTheme) @@ -74,7 +78,7 @@ func (s *Settings) LoadAppearanceScreen(w fyne.Window) fyne.CanvasObject { }) animations.Checked = !s.fyneSettings.DisableAnimations for _, c := range theme.PrimaryColorNames() { - b := newColorButton(c, theme.PrimaryColorNamed(c), s) + b := newPrimaryColorButton(c, s) s.colors = append(s.colors, b) } swatch := container.NewGridWithColumns(len(s.colors), s.colors...) @@ -102,8 +106,8 @@ func (s *Settings) LoadAppearanceScreen(w fyne.Window) fyne.CanvasObject { } func (s *Settings) chooseTheme(name string) { - if name == systemThemeName { - name = "" + if name == themeNameSystemLabel { + name = themeNameSystem } s.fyneSettings.ThemeName = name @@ -167,22 +171,21 @@ func (s *Settings) saveToFile(path string) error { return os.WriteFile(path, data, 0644) } -type colorButton struct { +type primaryColorButton struct { widget.BaseWidget - name string - color color.Color + name string s *Settings } -func newColorButton(n string, c color.Color, s *Settings) *colorButton { - b := &colorButton{name: n, color: c, s: s} +func newPrimaryColorButton(name string, s *Settings) *primaryColorButton { + b := &primaryColorButton{name: name, s: s} b.ExtendBaseWidget(b) return b } -func (c *colorButton) CreateRenderer() fyne.WidgetRenderer { - r := canvas.NewRectangle(c.color) +func (c *primaryColorButton) CreateRenderer() fyne.WidgetRenderer { + r := canvas.NewRectangle(theme.PrimaryColorNamed(c.name)) r.CornerRadius = theme.SelectionRadiusSize() r.StrokeWidth = 5 @@ -190,10 +193,10 @@ func (c *colorButton) CreateRenderer() fyne.WidgetRenderer { r.StrokeColor = theme.PrimaryColor() } - return &colorRenderer{c: c, rect: r, objs: []fyne.CanvasObject{r}} + return &primaryColorButtonRenderer{c: c, rect: r, objs: []fyne.CanvasObject{r}} } -func (c *colorButton) Tapped(_ *fyne.PointEvent) { +func (c *primaryColorButton) Tapped(_ *fyne.PointEvent) { c.s.fyneSettings.PrimaryColor = c.name for _, child := range c.s.colors { child.Refresh() @@ -202,37 +205,37 @@ func (c *colorButton) Tapped(_ *fyne.PointEvent) { c.s.refreshPreview() } -type colorRenderer struct { - c *colorButton +type primaryColorButtonRenderer struct { + c *primaryColorButton rect *canvas.Rectangle objs []fyne.CanvasObject } -func (c *colorRenderer) Layout(s fyne.Size) { +func (c *primaryColorButtonRenderer) Layout(s fyne.Size) { c.rect.Resize(s) } -func (c *colorRenderer) MinSize() fyne.Size { +func (c *primaryColorButtonRenderer) MinSize() fyne.Size { return fyne.NewSize(20, 32) } -func (c *colorRenderer) Refresh() { +func (c *primaryColorButtonRenderer) Refresh() { if c.c.name == c.c.s.fyneSettings.PrimaryColor { c.rect.StrokeColor = theme.PrimaryColor() } else { c.rect.StrokeColor = color.Transparent } - c.rect.FillColor = c.c.color + c.rect.FillColor = theme.PrimaryColorNamed(c.c.name) c.rect.CornerRadius = theme.SelectionRadiusSize() c.rect.Refresh() } -func (c *colorRenderer) Objects() []fyne.CanvasObject { +func (c *primaryColorButtonRenderer) Objects() []fyne.CanvasObject { return c.objs } -func (c *colorRenderer) Destroy() { +func (c *primaryColorButtonRenderer) Destroy() { } type previewTheme struct { @@ -242,8 +245,11 @@ type previewTheme struct { func (p *previewTheme) Color(n fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { variant := theme.VariantDark - if p.s.fyneSettings.ThemeName == "light" { + switch p.s.fyneSettings.ThemeName { + case themeNameLight: variant = theme.VariantLight + case themeNameSystem: + variant = internalapp.DefaultVariant() } switch n { diff --git a/container/apptabs_extend_test.go b/container/apptabs_extend_test.go index f9fd6b6fff..1f53648823 100644 --- a/container/apptabs_extend_test.go +++ b/container/apptabs_extend_test.go @@ -30,31 +30,31 @@ func TestAppTabs_Extended_Tapped(t *testing.T) { NewTabItem("Test2", widget.NewLabel("Test2")), ) tabs.Resize(fyne.NewSize(150, 150)) // Ensure AppTabs is big enough to show both tab buttons - r := test.WidgetRenderer(tabs).(*appTabsRenderer) + r := test.TempWidgetRenderer(t, tabs).(*appTabsRenderer) tab1 := r.bar.Objects[0].(*fyne.Container).Objects[0].(*tabButton) tab2 := r.bar.Objects[0].(*fyne.Container).Objects[1].(*tabButton) require.Equal(t, 0, tabs.SelectedIndex()) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) tab2.Tapped(&fyne.PointEvent{}) assert.Equal(t, 1, tabs.SelectedIndex()) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.False(t, tabs.Items[0].Content.Visible()) assert.True(t, tabs.Items[1].Content.Visible()) tab2.Tapped(&fyne.PointEvent{}) assert.Equal(t, 1, tabs.SelectedIndex()) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.False(t, tabs.Items[0].Content.Visible()) assert.True(t, tabs.Items[1].Content.Visible()) tab1.Tapped(&fyne.PointEvent{}) assert.Equal(t, 0, tabs.SelectedIndex()) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.True(t, tabs.Items[0].Content.Visible()) assert.False(t, tabs.Items[1].Content.Visible()) } diff --git a/container/apptabs_test.go b/container/apptabs_test.go index 5a18252f1c..7958e48412 100644 --- a/container/apptabs_test.go +++ b/container/apptabs_test.go @@ -39,7 +39,7 @@ func TestAppTabs_Empty(t *testing.T) { tabs = &AppTabs{} assert.Equal(t, 0, len(tabs.Items)) assert.Nil(t, tabs.Selected()) - assert.NotNil(t, test.WidgetRenderer(tabs)) // doesn't crash + assert.NotNil(t, test.TempWidgetRenderer(t, tabs)) // doesn't crash } func TestAppTabs_Hidden_AsChild(t *testing.T) { @@ -183,7 +183,7 @@ func TestAppTabs_DisableIndex(t *testing.T) { func TestAppTabs_ShowAfterAdd(t *testing.T) { tabs := NewAppTabs() - renderer := test.WidgetRenderer(tabs).(*appTabsRenderer) + renderer := test.TempWidgetRenderer(t, tabs).(*appTabsRenderer) assert.True(t, renderer.indicator.Hidden) diff --git a/container/doctabs_extend_test.go b/container/doctabs_extend_test.go index bd331730dd..00eb83d9e0 100644 --- a/container/doctabs_extend_test.go +++ b/container/doctabs_extend_test.go @@ -30,32 +30,32 @@ func TestDocTabs_Extended_Tapped(t *testing.T) { NewTabItem("Test2", widget.NewLabel("Test2")), ) tabs.Resize(fyne.NewSize(150, 150)) // Ensure DocTabs is big enough to show both tab buttons - r := test.WidgetRenderer(tabs).(*docTabsRenderer) + r := test.TempWidgetRenderer(t, tabs).(*docTabsRenderer) buttons := r.bar.Objects[0].(*Scroll).Content.(*fyne.Container).Objects tab1 := buttons[0].(*tabButton) tab2 := buttons[1].(*tabButton) require.Equal(t, 0, tabs.SelectedIndex()) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) tab2.Tapped(&fyne.PointEvent{}) assert.Equal(t, 1, tabs.SelectedIndex()) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.False(t, tabs.Items[0].Content.Visible()) assert.True(t, tabs.Items[1].Content.Visible()) tab2.Tapped(&fyne.PointEvent{}) assert.Equal(t, 1, tabs.SelectedIndex()) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.False(t, tabs.Items[0].Content.Visible()) assert.True(t, tabs.Items[1].Content.Visible()) tab1.Tapped(&fyne.PointEvent{}) assert.Equal(t, 0, tabs.SelectedIndex()) - require.Equal(t, theme.PrimaryColor(), test.WidgetRenderer(tab1).(*tabButtonRenderer).label.Color) - require.Equal(t, theme.ForegroundColor(), test.WidgetRenderer(tab2).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.PrimaryColor(), test.TempWidgetRenderer(t, tab1).(*tabButtonRenderer).label.Color) + require.Equal(t, theme.ForegroundColor(), test.TempWidgetRenderer(t, tab2).(*tabButtonRenderer).label.Color) assert.True(t, tabs.Items[0].Content.Visible()) assert.False(t, tabs.Items[1].Content.Visible()) } diff --git a/container/doctabs_test.go b/container/doctabs_test.go index a6cb924393..e0c24dff14 100644 --- a/container/doctabs_test.go +++ b/container/doctabs_test.go @@ -40,7 +40,7 @@ func TestDocTabs_Empty(t *testing.T) { tabs = &container.DocTabs{} assert.Equal(t, 0, len(tabs.Items)) assert.Nil(t, tabs.Selected()) - assert.NotNil(t, test.WidgetRenderer(tabs)) // doesn't crash + assert.NotNil(t, test.TempWidgetRenderer(t, tabs)) // doesn't crash } func TestDocTabs_Hidden_AsChild(t *testing.T) { diff --git a/container/multiplewindows_test.go b/container/multiplewindows_test.go index ea895bdfdb..c8901abb3a 100644 --- a/container/multiplewindows_test.go +++ b/container/multiplewindows_test.go @@ -20,7 +20,7 @@ func TestMultipleWindows_Add(t *testing.T) { func TestMultipleWindows_Drag(t *testing.T) { w := NewInnerWindow("1", widget.NewLabel("Inside")) m := NewMultipleWindows(w) - _ = test.WidgetRenderer(m) // initialise display + _ = test.TempWidgetRenderer(t, m) // initialise display assert.Equal(t, 1, len(m.Windows)) assert.True(t, w.Position().IsZero()) diff --git a/container/split_test.go b/container/split_test.go index 1f7d5c6c15..a7c1ee52d1 100644 --- a/container/split_test.go +++ b/container/split_test.go @@ -290,7 +290,7 @@ func TestSplitContainer_divider_drag(t *testing.T) { t.Run("Horizontal", func(t *testing.T) { split := NewHSplit(objA, objB) split.Resize(fyne.NewSize(108, 108)) - divider := test.WidgetRenderer(split).(*splitContainerRenderer).divider + divider := test.TempWidgetRenderer(t, split).(*splitContainerRenderer).divider assert.Equal(t, 0.5, split.Offset) divider.Dragged(&fyne.DragEvent{ @@ -324,7 +324,7 @@ func TestSplitContainer_divider_drag(t *testing.T) { t.Run("Vertical", func(t *testing.T) { split := NewVSplit(objA, objB) split.Resize(fyne.NewSize(108, 108)) - divider := test.WidgetRenderer(split).(*splitContainerRenderer).divider + divider := test.TempWidgetRenderer(t, split).(*splitContainerRenderer).divider assert.Equal(t, 0.5, split.Offset) divider.Dragged(&fyne.DragEvent{ @@ -366,7 +366,7 @@ func TestSplitContainer_divider_drag_StartOffsetLessThanMinSize(t *testing.T) { t.Run("Horizontal", func(t *testing.T) { split := NewHSplit(objA, objB) split.Resize(fyne.NewSize(108, 108)) - divider := test.WidgetRenderer(split).(*splitContainerRenderer).divider + divider := test.TempWidgetRenderer(t, split).(*splitContainerRenderer).divider t.Run("Leading", func(t *testing.T) { split.SetOffset(0.1) @@ -391,7 +391,7 @@ func TestSplitContainer_divider_drag_StartOffsetLessThanMinSize(t *testing.T) { t.Run("Vertical", func(t *testing.T) { split := NewVSplit(objA, objB) split.Resize(fyne.NewSize(108, 108)) - divider := test.WidgetRenderer(split).(*splitContainerRenderer).divider + divider := test.TempWidgetRenderer(t, split).(*splitContainerRenderer).divider t.Run("Leading", func(t *testing.T) { split.SetOffset(0.1) diff --git a/container/testdata/apptabs/desktop/change_icon_change_selected.xml b/container/testdata/apptabs/desktop/change_icon_change_selected.xml index bce4d41e1f..809f31f8b2 100644 --- a/container/testdata/apptabs/desktop/change_icon_change_selected.xml +++ b/container/testdata/apptabs/desktop/change_icon_change_selected.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/apptabs/desktop/change_icon_change_unselected.xml b/container/testdata/apptabs/desktop/change_icon_change_unselected.xml index 508d5b0ce2..b3e2bf2b41 100644 --- a/container/testdata/apptabs/desktop/change_icon_change_unselected.xml +++ b/container/testdata/apptabs/desktop/change_icon_change_unselected.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/apptabs/desktop/change_icon_initial.xml b/container/testdata/apptabs/desktop/change_icon_initial.xml index dac9b30f2d..e2314b8cb7 100644 --- a/container/testdata/apptabs/desktop/change_icon_initial.xml +++ b/container/testdata/apptabs/desktop/change_icon_initial.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/apptabs/desktop/tapped_overflow_tabs.xml b/container/testdata/apptabs/desktop/tapped_overflow_tabs.xml index d40d919d6a..72ad2b4349 100644 --- a/container/testdata/apptabs/desktop/tapped_overflow_tabs.xml +++ b/container/testdata/apptabs/desktop/tapped_overflow_tabs.xml @@ -26,8 +26,8 @@ - - + + diff --git a/container/testdata/apptabs/mobile/change_icon_change_selected.xml b/container/testdata/apptabs/mobile/change_icon_change_selected.xml index 22bf6138bb..8d3c2d95f5 100644 --- a/container/testdata/apptabs/mobile/change_icon_change_selected.xml +++ b/container/testdata/apptabs/mobile/change_icon_change_selected.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/apptabs/mobile/change_icon_change_unselected.xml b/container/testdata/apptabs/mobile/change_icon_change_unselected.xml index c0f4d26cff..fdb7fabb40 100644 --- a/container/testdata/apptabs/mobile/change_icon_change_unselected.xml +++ b/container/testdata/apptabs/mobile/change_icon_change_unselected.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/apptabs/mobile/change_icon_initial.xml b/container/testdata/apptabs/mobile/change_icon_initial.xml index d5c86c237f..cdb284c656 100644 --- a/container/testdata/apptabs/mobile/change_icon_initial.xml +++ b/container/testdata/apptabs/mobile/change_icon_initial.xml @@ -7,7 +7,7 @@ - + diff --git a/container/testdata/doctabs/desktop/change_icon_change_selected.xml b/container/testdata/doctabs/desktop/change_icon_change_selected.xml index 3ec2846a82..1a06ef8d0f 100644 --- a/container/testdata/doctabs/desktop/change_icon_change_selected.xml +++ b/container/testdata/doctabs/desktop/change_icon_change_selected.xml @@ -8,7 +8,7 @@ - + diff --git a/container/testdata/doctabs/desktop/change_icon_change_unselected.xml b/container/testdata/doctabs/desktop/change_icon_change_unselected.xml index 85a5e92da4..93302e3bc1 100644 --- a/container/testdata/doctabs/desktop/change_icon_change_unselected.xml +++ b/container/testdata/doctabs/desktop/change_icon_change_unselected.xml @@ -8,7 +8,7 @@ - + diff --git a/container/testdata/doctabs/desktop/change_icon_initial.xml b/container/testdata/doctabs/desktop/change_icon_initial.xml index 0053a49fc9..d637f4bbdc 100644 --- a/container/testdata/doctabs/desktop/change_icon_initial.xml +++ b/container/testdata/doctabs/desktop/change_icon_initial.xml @@ -8,7 +8,7 @@ - + diff --git a/container/testdata/doctabs/desktop/single_custom_theme.png b/container/testdata/doctabs/desktop/single_custom_theme.png index 1ab9a5491c..a020f40b42 100644 Binary files a/container/testdata/doctabs/desktop/single_custom_theme.png and b/container/testdata/doctabs/desktop/single_custom_theme.png differ diff --git a/container/testdata/doctabs/desktop/single_initial.png b/container/testdata/doctabs/desktop/single_initial.png index 1bbe79ca65..1e711173c1 100644 Binary files a/container/testdata/doctabs/desktop/single_initial.png and b/container/testdata/doctabs/desktop/single_initial.png differ diff --git a/container/testdata/doctabs/desktop/tapped_all_tabs.xml b/container/testdata/doctabs/desktop/tapped_all_tabs.xml index d3449874c1..32f72d36d3 100644 --- a/container/testdata/doctabs/desktop/tapped_all_tabs.xml +++ b/container/testdata/doctabs/desktop/tapped_all_tabs.xml @@ -49,19 +49,19 @@ - - - + + + - - - - - + + + + + - + @@ -76,16 +76,16 @@ Another - + - + - - - + + + diff --git a/container/testdata/doctabs/mobile/change_content_change_hidden.xml b/container/testdata/doctabs/mobile/change_content_change_hidden.xml index f41fbd6321..5964894592 100644 --- a/container/testdata/doctabs/mobile/change_content_change_hidden.xml +++ b/container/testdata/doctabs/mobile/change_content_change_hidden.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/change_content_change_visible.xml b/container/testdata/doctabs/mobile/change_content_change_visible.xml index f41fbd6321..5964894592 100644 --- a/container/testdata/doctabs/mobile/change_content_change_visible.xml +++ b/container/testdata/doctabs/mobile/change_content_change_visible.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/change_content_initial.xml b/container/testdata/doctabs/mobile/change_content_initial.xml index 7ad4ba15e8..1142de040c 100644 --- a/container/testdata/doctabs/mobile/change_content_initial.xml +++ b/container/testdata/doctabs/mobile/change_content_initial.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/change_icon_change_selected.xml b/container/testdata/doctabs/mobile/change_icon_change_selected.xml index 2b941ffa5d..2c2b150796 100644 --- a/container/testdata/doctabs/mobile/change_icon_change_selected.xml +++ b/container/testdata/doctabs/mobile/change_icon_change_selected.xml @@ -12,9 +12,9 @@ - + - + diff --git a/container/testdata/doctabs/mobile/change_icon_change_unselected.xml b/container/testdata/doctabs/mobile/change_icon_change_unselected.xml index 118e08973a..918e54e500 100644 --- a/container/testdata/doctabs/mobile/change_icon_change_unselected.xml +++ b/container/testdata/doctabs/mobile/change_icon_change_unselected.xml @@ -12,9 +12,9 @@ - + - + diff --git a/container/testdata/doctabs/mobile/change_icon_initial.xml b/container/testdata/doctabs/mobile/change_icon_initial.xml index 7805daf3a1..bd033ee873 100644 --- a/container/testdata/doctabs/mobile/change_icon_initial.xml +++ b/container/testdata/doctabs/mobile/change_icon_initial.xml @@ -12,9 +12,9 @@ - + - + diff --git a/container/testdata/doctabs/mobile/change_label_change_selected.xml b/container/testdata/doctabs/mobile/change_label_change_selected.xml index 698e5cd164..d634ea9272 100644 --- a/container/testdata/doctabs/mobile/change_label_change_selected.xml +++ b/container/testdata/doctabs/mobile/change_label_change_selected.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/change_label_change_unselected.xml b/container/testdata/doctabs/mobile/change_label_change_unselected.xml index 794dfee63a..1b54f603f3 100644 --- a/container/testdata/doctabs/mobile/change_label_change_unselected.xml +++ b/container/testdata/doctabs/mobile/change_label_change_unselected.xml @@ -13,7 +13,7 @@ New 2 - + diff --git a/container/testdata/doctabs/mobile/change_label_initial.xml b/container/testdata/doctabs/mobile/change_label_initial.xml index 7ad4ba15e8..1142de040c 100644 --- a/container/testdata/doctabs/mobile/change_label_initial.xml +++ b/container/testdata/doctabs/mobile/change_label_initial.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/dynamic_appended.xml b/container/testdata/doctabs/mobile/dynamic_appended.xml index 182d0bd0b8..630bb963ec 100644 --- a/container/testdata/doctabs/mobile/dynamic_appended.xml +++ b/container/testdata/doctabs/mobile/dynamic_appended.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/dynamic_appended_another_three.xml b/container/testdata/doctabs/mobile/dynamic_appended_another_three.xml index 78a0fda310..2687412d91 100644 --- a/container/testdata/doctabs/mobile/dynamic_appended_another_three.xml +++ b/container/testdata/doctabs/mobile/dynamic_appended_another_three.xml @@ -13,19 +13,19 @@ Test3 - + Test4 - + Test5 - + diff --git a/container/testdata/doctabs/mobile/dynamic_replaced_completely.xml b/container/testdata/doctabs/mobile/dynamic_replaced_completely.xml index b99fddaca1..47e239d34d 100644 --- a/container/testdata/doctabs/mobile/dynamic_replaced_completely.xml +++ b/container/testdata/doctabs/mobile/dynamic_replaced_completely.xml @@ -13,13 +13,13 @@ Test7 - + Test8 - + diff --git a/container/testdata/doctabs/mobile/hover_none.xml b/container/testdata/doctabs/mobile/hover_none.xml index f5da04b3bd..aa57bfba17 100644 --- a/container/testdata/doctabs/mobile/hover_none.xml +++ b/container/testdata/doctabs/mobile/hover_none.xml @@ -13,7 +13,7 @@ Test2 - + diff --git a/container/testdata/doctabs/mobile/tab_location_bottom.xml b/container/testdata/doctabs/mobile/tab_location_bottom.xml index b86c07409c..874f391f33 100644 --- a/container/testdata/doctabs/mobile/tab_location_bottom.xml +++ b/container/testdata/doctabs/mobile/tab_location_bottom.xml @@ -13,13 +13,13 @@ Test2 - + Test3 - + diff --git a/container/testdata/doctabs/mobile/tab_location_top.xml b/container/testdata/doctabs/mobile/tab_location_top.xml index ab8439e007..31724f57c1 100644 --- a/container/testdata/doctabs/mobile/tab_location_top.xml +++ b/container/testdata/doctabs/mobile/tab_location_top.xml @@ -13,13 +13,13 @@ Test2 - + Test3 - + diff --git a/container/testdata/doctabs/mobile/tapped_all_tabs.xml b/container/testdata/doctabs/mobile/tapped_all_tabs.xml index 2991fc6c3b..6235bee898 100644 --- a/container/testdata/doctabs/mobile/tapped_all_tabs.xml +++ b/container/testdata/doctabs/mobile/tapped_all_tabs.xml @@ -7,19 +7,19 @@ Test1 - + Test2 - + Test3 - + @@ -61,19 +61,19 @@ - - - + + + - - - - - + + + + + - + @@ -88,16 +88,16 @@ Another - + - + - - - + + + diff --git a/container/testdata/doctabs/mobile/tapped_create_tab.xml b/container/testdata/doctabs/mobile/tapped_create_tab.xml index e1dec74b47..2ef7396635 100644 --- a/container/testdata/doctabs/mobile/tapped_create_tab.xml +++ b/container/testdata/doctabs/mobile/tapped_create_tab.xml @@ -7,19 +7,19 @@ Test1 - + Test2 - + Test3 - + diff --git a/container/testdata/doctabs/mobile/tapped_first_selected.xml b/container/testdata/doctabs/mobile/tapped_first_selected.xml index 68414668e9..596f0de910 100644 --- a/container/testdata/doctabs/mobile/tapped_first_selected.xml +++ b/container/testdata/doctabs/mobile/tapped_first_selected.xml @@ -13,13 +13,13 @@ Test2 - + Test3 - + diff --git a/container/testdata/doctabs/mobile/tapped_second_selected.xml b/container/testdata/doctabs/mobile/tapped_second_selected.xml index ecd37da9b5..6ec1d9d20e 100644 --- a/container/testdata/doctabs/mobile/tapped_second_selected.xml +++ b/container/testdata/doctabs/mobile/tapped_second_selected.xml @@ -7,7 +7,7 @@ Test1 - + @@ -19,7 +19,7 @@ Test3 - + diff --git a/container/testdata/doctabs/mobile/tapped_third_selected.xml b/container/testdata/doctabs/mobile/tapped_third_selected.xml index 364fa3eda3..02c7c6bf7d 100644 --- a/container/testdata/doctabs/mobile/tapped_third_selected.xml +++ b/container/testdata/doctabs/mobile/tapped_third_selected.xml @@ -7,13 +7,13 @@ Test1 - + Test2 - + diff --git a/container/testdata/doctabs/mobile/theme_default.png b/container/testdata/doctabs/mobile/theme_default.png index 939c4a293a..69b644200f 100644 Binary files a/container/testdata/doctabs/mobile/theme_default.png and b/container/testdata/doctabs/mobile/theme_default.png differ diff --git a/container/testdata/doctabs/mobile/theme_ugly.png b/container/testdata/doctabs/mobile/theme_ugly.png index 1ab9a5491c..a020f40b42 100644 Binary files a/container/testdata/doctabs/mobile/theme_ugly.png and b/container/testdata/doctabs/mobile/theme_ugly.png differ diff --git a/container/testdata/theme/icon-other-theme.png b/container/testdata/theme/icon-other-theme.png new file mode 100644 index 0000000000..fc0a4953ae Binary files /dev/null and b/container/testdata/theme/icon-other-theme.png differ diff --git a/container/testdata/theme/icon-test-theme.png b/container/testdata/theme/icon-test-theme.png new file mode 100644 index 0000000000..fa8b431d5d Binary files /dev/null and b/container/testdata/theme/icon-test-theme.png differ diff --git a/container/testdata/theme/text-other-theme.png b/container/testdata/theme/text-other-theme.png new file mode 100644 index 0000000000..db0cb8c0f4 Binary files /dev/null and b/container/testdata/theme/text-other-theme.png differ diff --git a/container/testdata/theme/text-test-theme.png b/container/testdata/theme/text-test-theme.png new file mode 100644 index 0000000000..fe8a9db469 Binary files /dev/null and b/container/testdata/theme/text-test-theme.png differ diff --git a/container/theme.go b/container/theme.go index 7cc55d10f4..cd4e29c3d7 100644 --- a/container/theme.go +++ b/container/theme.go @@ -30,6 +30,7 @@ func NewThemeOverride(obj fyne.CanvasObject, th fyne.Theme) *ThemeOverride { t.ExtendBaseWidget(t) cache.OverrideTheme(obj, th) + obj.Refresh() // required as the widgets passed in could have been initially rendered with default theme return t } diff --git a/container/theme_test.go b/container/theme_test.go new file mode 100644 index 0000000000..36ae8fcecf --- /dev/null +++ b/container/theme_test.go @@ -0,0 +1,36 @@ +package container + +import ( + "image" + "testing" + + "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +func TestThemeOverride_Icons(t *testing.T) { + b := widget.NewButtonWithIcon("", theme.HomeIcon(), func() {}) + o := NewThemeOverride(b, test.Theme()) + w := test.NewWindow(o) + plain := w.Canvas().Capture().(*image.NRGBA) + test.AssertImageMatches(t, "theme/icon-test-theme.png", plain) + + o.Theme = test.NewTheme() + o.Refresh() + changed := w.Canvas().Capture().(*image.NRGBA) + test.AssertImageMatches(t, "theme/icon-other-theme.png", changed) +} + +func TestThemeOverride_Refresh(t *testing.T) { + b := widget.NewButton("Test", func() {}) + o := NewThemeOverride(b, test.Theme()) + w := test.NewWindow(o) + plain := w.Canvas().Capture().(*image.NRGBA) + test.AssertImageMatches(t, "theme/text-test-theme.png", plain) + + o.Theme = test.NewTheme() + o.Refresh() + changed := w.Canvas().Capture().(*image.NRGBA) + test.AssertImageMatches(t, "theme/text-other-theme.png", changed) +} diff --git a/dialog/color_picker.go b/dialog/color_picker.go index 410aa7715a..7040e8652d 100644 --- a/dialog/color_picker.go +++ b/dialog/color_picker.go @@ -12,29 +12,36 @@ import ( // newColorBasicPicker returns a component for selecting basic colors. func newColorBasicPicker(callback func(color.Color)) fyne.CanvasObject { - return newColorButtonBox([]color.Color{ - theme.PrimaryColorNamed(theme.ColorRed), - theme.PrimaryColorNamed(theme.ColorOrange), - theme.PrimaryColorNamed(theme.ColorYellow), - theme.PrimaryColorNamed(theme.ColorGreen), - theme.PrimaryColorNamed(theme.ColorBlue), - theme.PrimaryColorNamed(theme.ColorPurple), - theme.PrimaryColorNamed(theme.ColorBrown), - // theme.PrimaryColorNamed(theme.ColorGray), - }, theme.ColorChromaticIcon(), callback) + return newColorButtonBox( + stringsToColors( + "#f44336", // red + "#ff9800", // orange + "#ffeb3b", // yellow + "#8bc34a", // green + "#296ff6", // blue + "#9c27b0", // purple + "#795548", // brown + ), + theme.ColorChromaticIcon(), + callback, + ) } // newColorGreyscalePicker returns a component for selecting greyscale colors. func newColorGreyscalePicker(callback func(color.Color)) fyne.CanvasObject { - return newColorButtonBox(stringsToColors([]string{ - "#ffffff", - "#cccccc", - "#aaaaaa", - "#808080", - "#555555", - "#333333", - "#000000", - }...), theme.ColorAchromaticIcon(), callback) + return newColorButtonBox( + stringsToColors( + "#ffffff", + "#cccccc", + "#aaaaaa", + "#808080", + "#555555", + "#333333", + "#000000", + ), + theme.ColorAchromaticIcon(), + callback, + ) } // newColorRecentPicker returns a component for selecting recent colors. diff --git a/dialog/file.go b/dialog/file.go index b2377dd256..f6c0e6b50f 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -431,7 +431,7 @@ func (f *fileDialog) setLocation(dir fyne.URI) error { f.files.Unselect(f.selectedID) } if dir == nil { - return fmt.Errorf("failed to open nil directory") + return errors.New("failed to open nil directory") } list, err := storage.ListerForURI(dir) if err != nil { diff --git a/dialog/file_test.go b/dialog/file_test.go index 0c67fe767a..a81e329f39 100644 --- a/dialog/file_test.go +++ b/dialog/file_test.go @@ -214,17 +214,17 @@ func TestShowFileOpen(t *testing.T) { } files := ui.Objects[0].(*container.Split).Trailing.(*fyne.Container).Objects[1].(*container.Scroll).Content.(*fyne.Container).Objects[0].(*widget.GridWrap) - objects := test.WidgetRenderer(files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects + objects := test.TempWidgetRenderer(t, files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects assert.Greater(t, len(objects), 0) - fileName := test.WidgetRenderer(objects[0].(fyne.Widget)).Objects()[1].(*fileDialogItem).name + fileName := test.TempWidgetRenderer(t, objects[0].(fyne.Widget)).Objects()[1].(*fileDialogItem).name assert.Equal(t, "(Parent)", fileName) assert.True(t, open.Disabled()) var target *fileDialogItem id := 0 for i, icon := range objects { - item := test.WidgetRenderer(icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) + item := test.TempWidgetRenderer(t, icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) if item.dir == false { target = item id = i @@ -283,12 +283,12 @@ func TestHiddenFiles(t *testing.T) { assert.Equal(t, theme.SettingsIcon().Name(), optionsButton.Icon.Name()) files := ui.Objects[0].(*container.Split).Trailing.(*fyne.Container).Objects[1].(*container.Scroll).Content.(*fyne.Container).Objects[0].(*widget.GridWrap) - objects := test.WidgetRenderer(files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects + objects := test.TempWidgetRenderer(t, files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects assert.Greater(t, len(objects), 0) var target *fileDialogItem for _, icon := range objects { - item := test.WidgetRenderer(icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) + item := test.TempWidgetRenderer(t, icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) if item.name == ".hidden" { target = item } @@ -299,7 +299,7 @@ func TestHiddenFiles(t *testing.T) { d.dialog.refreshDir(d.dialog.dir) for _, icon := range objects { - item := test.WidgetRenderer(icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) + item := test.TempWidgetRenderer(t, icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) if item.name == ".hidden" { target = item } @@ -330,17 +330,17 @@ func TestShowFileSave(t *testing.T) { save := buttons.Objects[1].(*widget.Button) files := ui.Objects[0].(*container.Split).Trailing.(*fyne.Container).Objects[1].(*container.Scroll).Content.(*fyne.Container).Objects[0].(*widget.GridWrap) - objects := test.WidgetRenderer(files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects + objects := test.TempWidgetRenderer(t, files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects assert.Greater(t, len(objects), 0) - item := test.WidgetRenderer(objects[0].(fyne.Widget)).Objects()[1].(*fileDialogItem) + item := test.TempWidgetRenderer(t, objects[0].(fyne.Widget)).Objects()[1].(*fileDialogItem) assert.Equal(t, "(Parent)", item.name) assert.True(t, save.Disabled()) var target *fileDialogItem id := -1 for i, icon := range objects { - item := test.WidgetRenderer(icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) + item := test.TempWidgetRenderer(t, icon.(fyne.Widget)).Objects()[1].(*fileDialogItem) if item.dir == false { target = item id = i diff --git a/dialog/fileitem_test.go b/dialog/fileitem_test.go index 1187847fd5..6882ec05b2 100644 --- a/dialog/fileitem_test.go +++ b/dialog/fileitem_test.go @@ -115,14 +115,14 @@ func TestFileItem_Wrap(t *testing.T) { _ = f.makeUI() item := f.newFileItem(storage.NewFileURI("/path/to/filename.txt"), false, false) item.Resize(item.MinSize()) - label := test.WidgetRenderer(item).(*fileItemRenderer).text + label := test.TempWidgetRenderer(t, item).(*fileItemRenderer).text assert.Equal(t, "filename", label.Text) - texts := test.WidgetRenderer(label).Objects() + texts := test.TempWidgetRenderer(t, label).Objects() assert.Equal(t, 1, len(texts)) item.setLocation(storage.NewFileURI("/path/to/averylongfilename.svg"), false, false) - rich := test.WidgetRenderer(label).Objects()[0].(*widget.RichText) - texts = test.WidgetRenderer(rich).Objects() + rich := test.TempWidgetRenderer(t, label).Objects()[0].(*widget.RichText) + texts = test.TempWidgetRenderer(t, rich).Objects() assert.Equal(t, 2, len(texts)) assert.Equal(t, "averylon", texts[0].(*canvas.Text).Text) } diff --git a/dialog/folder_test.go b/dialog/folder_test.go index 768caf9ac5..08c4a4b0a8 100644 --- a/dialog/folder_test.go +++ b/dialog/folder_test.go @@ -44,8 +44,8 @@ func TestShowFolderOpen(t *testing.T) { files := ui.Objects[0].(*container.Split).Trailing.(*fyne.Container).Objects[1].(*container.Scroll).Content.(*fyne.Container).Objects[0].(*widget.GridWrap) assert.Greater(t, len(d.dialog.data), 0) - item := test.WidgetRenderer(files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects[0] - fileName := test.WidgetRenderer(item.(fyne.Widget)).Objects()[1].(*fileDialogItem).name + item := test.TempWidgetRenderer(t, files).Objects()[0].(*container.Scroll).Content.(*fyne.Container).Objects[0] + fileName := test.TempWidgetRenderer(t, item.(fyne.Widget)).Objects()[1].(*fileDialogItem).name assert.Equal(t, "(Parent)", fileName) assert.False(t, open.Disabled()) diff --git a/dialog/testdata/color/button_layout_primary.png b/dialog/testdata/color/button_layout_primary.png index cef550e738..4c101d2907 100644 Binary files a/dialog/testdata/color/button_layout_primary.png and b/dialog/testdata/color/button_layout_primary.png differ diff --git a/dialog/testdata/color/button_layout_primary_hovered.png b/dialog/testdata/color/button_layout_primary_hovered.png index 958ab0f851..0cd7354861 100644 Binary files a/dialog/testdata/color/button_layout_primary_hovered.png and b/dialog/testdata/color/button_layout_primary_hovered.png differ diff --git a/dialog/testdata/color/dialog_expanded_theme_default.png b/dialog/testdata/color/dialog_expanded_theme_default.png index 357f62f8c0..193425c601 100644 Binary files a/dialog/testdata/color/dialog_expanded_theme_default.png and b/dialog/testdata/color/dialog_expanded_theme_default.png differ diff --git a/dialog/testdata/color/dialog_expanded_theme_ugly.png b/dialog/testdata/color/dialog_expanded_theme_ugly.png index 9605d2c423..d07efd46e4 100644 Binary files a/dialog/testdata/color/dialog_expanded_theme_ugly.png and b/dialog/testdata/color/dialog_expanded_theme_ugly.png differ diff --git a/dialog/testdata/color/dialog_recents_theme_default.png b/dialog/testdata/color/dialog_recents_theme_default.png index fa7fe71565..9ba37b71b8 100644 Binary files a/dialog/testdata/color/dialog_recents_theme_default.png and b/dialog/testdata/color/dialog_recents_theme_default.png differ diff --git a/dialog/testdata/color/dialog_recents_theme_ugly.png b/dialog/testdata/color/dialog_recents_theme_ugly.png index 0012c7c46b..b3bf6657b8 100644 Binary files a/dialog/testdata/color/dialog_recents_theme_ugly.png and b/dialog/testdata/color/dialog_recents_theme_ugly.png differ diff --git a/dialog/testdata/color/dialog_simple_recents_theme_default.png b/dialog/testdata/color/dialog_simple_recents_theme_default.png index 62f23e9ea7..badd6b9b87 100644 Binary files a/dialog/testdata/color/dialog_simple_recents_theme_default.png and b/dialog/testdata/color/dialog_simple_recents_theme_default.png differ diff --git a/dialog/testdata/color/dialog_simple_recents_theme_ugly.png b/dialog/testdata/color/dialog_simple_recents_theme_ugly.png index 259a6e9ff1..2db9021fa4 100644 Binary files a/dialog/testdata/color/dialog_simple_recents_theme_ugly.png and b/dialog/testdata/color/dialog_simple_recents_theme_ugly.png differ diff --git a/dialog/testdata/color/dialog_simple_theme_default.png b/dialog/testdata/color/dialog_simple_theme_default.png index 99ca39a8a4..520b530f40 100644 Binary files a/dialog/testdata/color/dialog_simple_theme_default.png and b/dialog/testdata/color/dialog_simple_theme_default.png differ diff --git a/dialog/testdata/color/dialog_simple_theme_ugly.png b/dialog/testdata/color/dialog_simple_theme_ugly.png index 9bfeb3dcc2..9f41f45caa 100644 Binary files a/dialog/testdata/color/dialog_simple_theme_ugly.png and b/dialog/testdata/color/dialog_simple_theme_ugly.png differ diff --git a/dialog/testdata/color/dialog_theme_default.png b/dialog/testdata/color/dialog_theme_default.png index 55b4d350d4..ad36e8ad72 100644 Binary files a/dialog/testdata/color/dialog_theme_default.png and b/dialog/testdata/color/dialog_theme_default.png differ diff --git a/dialog/testdata/color/dialog_theme_ugly.png b/dialog/testdata/color/dialog_theme_ugly.png index 55ced81678..c57ba5d0c2 100644 Binary files a/dialog/testdata/color/dialog_theme_ugly.png and b/dialog/testdata/color/dialog_theme_ugly.png differ diff --git a/dialog/testdata/color/picker_layout_advanced.png b/dialog/testdata/color/picker_layout_advanced.png index a80b729e54..980685c802 100644 Binary files a/dialog/testdata/color/picker_layout_advanced.png and b/dialog/testdata/color/picker_layout_advanced.png differ diff --git a/dialog/testdata/dialog-confirm-importance.png b/dialog/testdata/dialog-confirm-importance.png index abd5b438e3..2319b61f01 100644 Binary files a/dialog/testdata/dialog-confirm-importance.png and b/dialog/testdata/dialog-confirm-importance.png differ diff --git a/dialog/testdata/dialog-custom-confirm-importance.png b/dialog/testdata/dialog-custom-confirm-importance.png index e9cb35e6ba..35b941edae 100644 Binary files a/dialog/testdata/dialog-custom-confirm-importance.png and b/dialog/testdata/dialog-custom-confirm-importance.png differ diff --git a/dialog/testdata/dialog-custom-custom-buttons.xml b/dialog/testdata/dialog-custom-custom-buttons.xml index 9f1c2d3538..824616e45f 100644 --- a/dialog/testdata/dialog-custom-custom-buttons.xml +++ b/dialog/testdata/dialog-custom-custom-buttons.xml @@ -19,7 +19,7 @@ - + diff --git a/dialog/testdata/dialog-custom-default.png b/dialog/testdata/dialog-custom-default.png index 18773b4716..ec69121fb1 100644 Binary files a/dialog/testdata/dialog-custom-default.png and b/dialog/testdata/dialog-custom-default.png differ diff --git a/dialog/testdata/dialog-custom-no-buttons.xml b/dialog/testdata/dialog-custom-no-buttons.xml index 7dd3d90d3e..7b220c62e3 100644 --- a/dialog/testdata/dialog-custom-no-buttons.xml +++ b/dialog/testdata/dialog-custom-no-buttons.xml @@ -19,7 +19,7 @@ - + diff --git a/dialog/testdata/dialog-custom-ugly.png b/dialog/testdata/dialog-custom-ugly.png index e26582b123..d8a4a970d0 100644 Binary files a/dialog/testdata/dialog-custom-ugly.png and b/dialog/testdata/dialog-custom-ugly.png differ diff --git a/dialog/testdata/dialog-custom-without-buttons.png b/dialog/testdata/dialog-custom-without-buttons.png index 3036f5419d..d553308db2 100644 Binary files a/dialog/testdata/dialog-custom-without-buttons.png and b/dialog/testdata/dialog-custom-without-buttons.png differ diff --git a/dialog/testdata/dialog-onshow-theme-changed.png b/dialog/testdata/dialog-onshow-theme-changed.png index 4987baef63..70bd4a31bc 100644 Binary files a/dialog/testdata/dialog-onshow-theme-changed.png and b/dialog/testdata/dialog-onshow-theme-changed.png differ diff --git a/dialog/testdata/dialog-onshow-theme-default.png b/dialog/testdata/dialog-onshow-theme-default.png index e8035b0692..b49ed4133c 100644 Binary files a/dialog/testdata/dialog-onshow-theme-default.png and b/dialog/testdata/dialog-onshow-theme-default.png differ diff --git a/driver.go b/driver.go index b66691dd31..21c4906fb5 100644 --- a/driver.go +++ b/driver.go @@ -12,7 +12,8 @@ type Driver interface { // RenderedTextSize returns the size required to render the given string of specified // font size and style. It also returns the height to text baseline, measured from the top. - RenderedTextSize(text string, fontSize float32, style TextStyle) (size Size, baseline float32) + // 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(CanvasObject) Canvas diff --git a/go.mod b/go.mod index 74009fb7fe..e0ddf2e2d3 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module fyne.io/fyne/v2 go 1.19 require ( - fyne.io/systray v1.10.1-0.20231230205326-d160fd363db9 - github.com/BurntSushi/toml v1.3.2 + fyne.io/systray v1.10.1-0.20240611130111-26449f257a02 + github.com/BurntSushi/toml v1.4.0 github.com/fogleman/gg v1.3.0 github.com/fredbi/uri v1.1.0 github.com/fsnotify/fsnotify v1.7.0 @@ -12,13 +12,13 @@ require ( github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240306074159-ea2d69986ecb + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a github.com/go-ole/go-ole v1.2.6 github.com/go-text/render v0.1.0 github.com/go-text/typesetting v0.1.0 github.com/godbus/dbus/v5 v5.1.0 github.com/jackmordaunt/icns/v2 v2.2.6 - github.com/jeandeaual/go-locale v0.0.0-20240204043739-672d8d016d9a + github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 github.com/josephspurrier/goversioninfo v1.4.0 github.com/lucor/goinfo v0.9.0 github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 @@ -29,13 +29,13 @@ require ( github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.4.0 - github.com/yuin/goldmark v1.5.5 + github.com/yuin/goldmark v1.7.1 golang.org/x/image v0.14.0 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a - golang.org/x/mod v0.14.0 - golang.org/x/sys v0.19.0 - golang.org/x/text v0.14.0 - golang.org/x/tools v0.16.1 + golang.org/x/mod v0.17.0 + golang.org/x/sys v0.20.0 + golang.org/x/text v0.15.0 + golang.org/x/tools v0.21.0 golang.org/x/tools/go/vcs v0.1.0-deprecated ) @@ -49,7 +49,8 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 80a1d43366..0cdd2d6848 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -16,7 +15,6 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -38,15 +36,12 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -fyne.io/fyne/v2 v2.4.3/go.mod h1:1h3BKxmQYRJlr2g+RGVxedzr6vLVQ/AJmFWcF9CJnoQ= -fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= -fyne.io/systray v1.10.1-0.20231230205326-d160fd363db9 h1:E/gHmMVyk8TuI6JIgNIv/Qu1JABMVFBIkQ8lYRa5gkQ= -fyne.io/systray v1.10.1-0.20231230205326-d160fd363db9/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +fyne.io/systray v1.10.1-0.20240611130111-26449f257a02 h1:ukSAnDNGKtlve5Cj5cB0qU3UAPIf1tRi5dQ/BOtfAdQ= +fyne.io/systray v1.10.1-0.20240611130111-26449f257a02/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= @@ -83,16 +78,13 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0= github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4= github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg= -github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E= github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a h1:ybgRdYvAHTn93HW79bLiBiJwVL4jVeyGQRZMgImoeWs= github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a/go.mod h1:gsGA2dotD4v0SR6PmPCYvS9JuOeMwAtmfvDE7mbYXMY= github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk= @@ -103,19 +95,14 @@ github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVin github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240306074159-ea2d69986ecb h1:S9I8pIVT5JHKDvmI1vQ0qs5fqxzUfhcZm/YbUC/8k1k= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240306074159-ea2d69986ecb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc= github.com/go-text/render v0.1.0 h1:osrmVDZNHuP1RSu3pNG7Z77Sd2xSbcb/xWytAj9kyVs= github.com/go-text/render v0.1.0/go.mod h1:jqEuNMenrmj6QRnkdpeaP0oKGFLDNhDkVKwGjsWWYU4= -github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw= github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= -github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3 h1:levTnuLLUmpavLGbJYLJA7fQnKeS7P1eCdAlM+vReXk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -179,7 +166,6 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= @@ -187,13 +173,11 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY= -github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -220,8 +204,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackmordaunt/icns/v2 v2.2.6 h1:M7kg6pWRmB+SyCvM058cV2BlAz3MedOHy4e3j2i7FQg= github.com/jackmordaunt/icns/v2 v2.2.6/go.mod h1:DqlVnR5iafSphrId7aSD06r3jg0KRC9V6lEBBp504ZQ= -github.com/jeandeaual/go-locale v0.0.0-20240204043739-672d8d016d9a h1:0x2wuPZ+Cq1CmMDRtnrtqRVNoBCiNuJJQODX8jryeFI= -github.com/jeandeaual/go-locale v0.0.0-20240204043739-672d8d016d9a/go.mod h1:ecPgkvIJEbP1gI6YrO2ILUeS3LZxUV5ZuCy8y6K2RvY= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN+Zj1tDsJQy7mJlPlwGNQd9JZoPjObagf8= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -267,10 +251,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -288,11 +270,9 @@ github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxr github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -300,12 +280,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= -github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -313,11 +290,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -326,9 +301,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= -github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -349,12 +323,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -368,9 +337,6 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -388,7 +354,6 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -400,11 +365,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -437,22 +399,13 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -476,11 +429,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -495,7 +445,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -518,37 +467,20 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -557,14 +489,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -617,16 +543,12 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -697,9 +619,7 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -752,7 +672,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/animation/runner.go b/internal/animation/runner.go index e4278bfff0..b45ccac069 100644 --- a/internal/animation/runner.go +++ b/internal/animation/runner.go @@ -9,10 +9,24 @@ import ( // Runner is the main driver for animations package type Runner struct { - animationMutex sync.RWMutex - animations []*anim + // animationMutex synchronizes access to `animations` and `pendingAnimations` + // between the runner goroutine and calls to Start and Stop + animationMutex sync.RWMutex + + // animations is the list of animations that are being ticked in the current frame + animations []*anim + + // pendingAnimations is animations that have been started but not yet picked up + // by the runner goroutine to be ticked each frame pendingAnimations []*anim + // nextFrameAnimations is the list of animations that will be ticked in the next frame. + // It is accessed only by the runner goroutine and accumulates the continuing animations + // during a tick that are not completed, plus the pendingAnimations picked up at the end of the frame. + // At the end of a full frame of animations, the nextFrameAnimations slice is swapped with + // the current `animations` slice which is then cleared out, while holding the mutex. + nextFrameAnimations []*anim + runnerStarted bool } @@ -23,9 +37,19 @@ func (r *Runner) Start(a *fyne.Animation) { if !r.runnerStarted { r.runnerStarted = true + if r.animations == nil { + // initialize with excess capacity to avoid re-allocations + // on subsequent Starts + r.animations = make([]*anim, 0, 16) + } r.animations = append(r.animations, newAnim(a)) r.runAnimations() } else { + if r.pendingAnimations == nil { + // initialize with excess capacity to avoid re-allocations + // on subsequent Starts + r.pendingAnimations = make([]*anim, 0, 16) + } r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) } } @@ -63,24 +87,10 @@ func (r *Runner) Stop(a *fyne.Animation) { func (r *Runner) runAnimations() { draw := time.NewTicker(time.Second / 60) - go func() { for done := false; !done; { <-draw.C - r.animationMutex.Lock() - oldList := r.animations - r.animationMutex.Unlock() - newList := make([]*anim, 0, len(oldList)) - for _, a := range oldList { - if !a.isStopped() && r.tickAnimation(a) { - newList = append(newList, a) - } - } - r.animationMutex.Lock() - r.animations = append(newList, r.pendingAnimations...) - r.pendingAnimations = nil - done = len(r.animations) == 0 - r.animationMutex.Unlock() + done = r.runOneFrame() } r.animationMutex.Lock() r.runnerStarted = false @@ -89,6 +99,34 @@ func (r *Runner) runAnimations() { }() } +func (r *Runner) runOneFrame() (done bool) { + r.animationMutex.Lock() + oldList := r.animations + r.animationMutex.Unlock() + for _, a := range oldList { + if !a.isStopped() && r.tickAnimation(a) { + r.nextFrameAnimations = append(r.nextFrameAnimations, a) + } + } + + r.animationMutex.Lock() + // nil out old r.animations for re-use as next r.nextFrameAnimations + tmp := r.animations + for i := range tmp { + tmp[i] = nil + } + r.animations = append(r.nextFrameAnimations, r.pendingAnimations...) + r.nextFrameAnimations = tmp[:0] + // nil out r.pendingAnimations + for i := range r.pendingAnimations { + r.pendingAnimations[i] = nil + } + r.pendingAnimations = r.pendingAnimations[:0] + done = len(r.animations) == 0 + r.animationMutex.Unlock() + return done +} + // tickAnimation will process a frame of animation and return true if this should continue animating func (r *Runner) tickAnimation(a *anim) bool { if time.Now().After(a.end) { diff --git a/internal/animation/runner_test.go b/internal/animation/runner_test.go new file mode 100644 index 0000000000..428230cef0 --- /dev/null +++ b/internal/animation/runner_test.go @@ -0,0 +1,26 @@ +package animation + +import ( + "testing" + "time" + + "fyne.io/fyne/v2" +) + +func BenchmarkRunnerAllocs(b *testing.B) { + r := Runner{} + var fl float32 + // setup some animations + for i := 0; i < 10; i++ { + r.pendingAnimations = append(r.pendingAnimations, newAnim( + fyne.NewAnimation(1000*time.Second, func(f float32) { + fl = f + }))) + } + for n := 0; n < b.N; n++ { + r.runOneFrame() + } + + b.ReportAllocs() + fl = fl + 1 // dummy use of variable +} diff --git a/internal/app/lifecycle.go b/internal/app/lifecycle.go index 30dcbe36c8..f41aeda099 100644 --- a/internal/app/lifecycle.go +++ b/internal/app/lifecycle.go @@ -4,6 +4,7 @@ import ( "sync/atomic" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" ) var _ fyne.Lifecycle = (*Lifecycle)(nil) @@ -18,6 +19,8 @@ type Lifecycle struct { onStopped atomic.Pointer[func()] onStoppedHookExecuted func() + + eventQueue *async.UnboundedFuncChan } // SetOnStoppedHookExecuted is an internal function that lets Fyne schedule a clean-up after @@ -100,3 +103,36 @@ func (l *Lifecycle) OnStopped() func() { stopHook() } } + +// DestroyEventQueue destroys the event queue. +func (l *Lifecycle) DestroyEventQueue() { + l.eventQueue.Close() +} + +// InitEventQueue initializes the event queue. +func (l *Lifecycle) InitEventQueue() { + // This channel should be closed when the window is closed. + l.eventQueue = async.NewUnboundedFuncChan() +} + +// QueueEvent uses this method to queue up a callback that handles an event. This ensures +// user interaction events for a given window are processed in order. +func (l *Lifecycle) QueueEvent(fn func()) { + l.eventQueue.In() <- fn +} + +// RunEventQueue runs the event queue. This should called inside a go routine. +// This function blocks. +func (l *Lifecycle) RunEventQueue() { + for fn := range l.eventQueue.Out() { + fn() + } +} + +// WaitForEvents wait for all the events. +func (l *Lifecycle) WaitForEvents() { + done := make(chan struct{}) + + l.eventQueue.In() <- func() { done <- struct{}{} } + <-done +} diff --git a/internal/app/lifecycle_test.go b/internal/app/lifecycle_test.go index 4c5e25cdeb..f7ba9468c4 100644 --- a/internal/app/lifecycle_test.go +++ b/internal/app/lifecycle_test.go @@ -15,7 +15,10 @@ func TestLifecycle(t *testing.T) { assert.Nil(t, life.OnStarted()) assert.Nil(t, life.OnStopped()) - var entered, exited, start, stop, hookedStop bool + var entered, exited, start, stop, hookedStop, called bool + life.InitEventQueue() + go life.RunEventQueue() + life.QueueEvent(func() { called = true }) life.SetOnEnteredForeground(func() { entered = true }) life.OnEnteredForeground()() assert.True(t, entered) @@ -48,4 +51,8 @@ func TestLifecycle(t *testing.T) { assert.Nil(t, life.OnExitedForeground()) assert.Nil(t, life.OnStarted()) assert.Nil(t, life.OnStopped()) + + life.WaitForEvents() + life.DestroyEventQueue() + assert.True(t, called) } diff --git a/internal/app/theme_darwin.go b/internal/app/theme_darwin.go new file mode 100644 index 0000000000..84f5828981 --- /dev/null +++ b/internal/app/theme_darwin.go @@ -0,0 +1,27 @@ +//go:build !ios && !wasm && !test_web_driver + +package app + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#include + +bool isDarkMode(); +*/ +import "C" +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + if C.isDarkMode() { + return theme.VariantDark + } + return theme.VariantLight +} diff --git a/internal/app/theme_darwin.m b/internal/app/theme_darwin.m new file mode 100644 index 0000000000..c4175dac33 --- /dev/null +++ b/internal/app/theme_darwin.m @@ -0,0 +1,8 @@ +//go:build !ios && !wasm && !test_web_driver + +#import + +bool isDarkMode() { + NSString *style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; + return [@"Dark" isEqualToString:style]; +} diff --git a/internal/app/theme_mobile.go b/internal/app/theme_mobile.go new file mode 100644 index 0000000000..fc10428074 --- /dev/null +++ b/internal/app/theme_mobile.go @@ -0,0 +1,18 @@ +//go:build android || ios || mobile + +package app + +import ( + "fyne.io/fyne/v2" +) + +// SystemTheme contains the system’s theme variant. +// It is intended for internal use, only! +var SystemTheme fyne.ThemeVariant + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return SystemTheme +} diff --git a/internal/app/theme_other.go b/internal/app/theme_other.go new file mode 100644 index 0000000000..f2d69da073 --- /dev/null +++ b/internal/app/theme_other.go @@ -0,0 +1,15 @@ +//go:build !linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return theme.VariantDark +} diff --git a/app/app_theme_wasm.go b/internal/app/theme_wasm.go similarity index 55% rename from app/app_theme_wasm.go rename to internal/app/theme_wasm.go index 59c7045ce3..427ae3fec6 100644 --- a/app/app_theme_wasm.go +++ b/internal/app/theme_wasm.go @@ -1,4 +1,4 @@ -//go:build !ci && wasm +//go:build wasm package app @@ -9,7 +9,10 @@ import ( "fyne.io/fyne/v2/theme" ) -func defaultVariant() fyne.ThemeVariant { +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { matches := js.Global().Call("matchMedia", "(prefers-color-scheme: dark)") if matches.Truthy() { if matches.Get("matches").Bool() { diff --git a/internal/app/theme_web.go b/internal/app/theme_web.go new file mode 100644 index 0000000000..310317c0c6 --- /dev/null +++ b/internal/app/theme_web.go @@ -0,0 +1,15 @@ +//go:build !wasm && test_web_driver + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return theme.VariantDark +} diff --git a/internal/app/theme_windows.go b/internal/app/theme_windows.go new file mode 100644 index 0000000000..f347bc5178 --- /dev/null +++ b/internal/app/theme_windows.go @@ -0,0 +1,35 @@ +//go:build !android && !ios && !wasm && !test_web_driver + +package app + +import ( + "golang.org/x/sys/windows/registry" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + if isDark() { + return theme.VariantDark + } + return theme.VariantLight +} + +func isDark() bool { + k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { // older version of Windows will not have this key + return false + } + defer k.Close() + + useLight, _, err := k.GetIntegerValue("AppsUseLightTheme") + if err != nil { // older version of Windows will not have this value + return false + } + + return useLight == 0 +} diff --git a/internal/app/theme_xdg.go b/internal/app/theme_xdg.go new file mode 100644 index 0000000000..87634699c8 --- /dev/null +++ b/internal/app/theme_xdg.go @@ -0,0 +1,20 @@ +//go:build !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd) + +package app + +import ( + "sync/atomic" + + "fyne.io/fyne/v2" +) + +// CurrentVariant contains the system’s theme variant. +// It is intended for internal use, only! +var CurrentVariant atomic.Uint64 + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return fyne.ThemeVariant(CurrentVariant.Load()) +} diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index 72ae9c1892..adffe67209 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -25,7 +25,7 @@ func TestCacheClean(t *testing.T) { for k := 0; k < 2; k++ { tm.setTime(10, 10+k*10) for i := 0; i < 20; i++ { - SetSvg(fmt.Sprintf("%d%d", k, i), nil, i, i+1) + SetSvg(fmt.Sprintf("%d%d", k, i), nil, nil, i, i+1) Renderer(&dummyWidget{onDestroy: func() { destroyedRenderersCnt++ }}) diff --git a/internal/cache/svg.go b/internal/cache/svg.go index 20b17038ff..94911acfca 100644 --- a/internal/cache/svg.go +++ b/internal/cache/svg.go @@ -4,13 +4,15 @@ import ( "image" "sync" "time" + + "fyne.io/fyne/v2" ) var svgs = &sync.Map{} // make(map[string]*svgInfo) // GetSvg gets svg image from cache if it exists. -func GetSvg(name string, w int, h int) *image.NRGBA { - sinfo, ok := svgs.Load(name) +func GetSvg(name string, o fyne.CanvasObject, w int, h int) *image.NRGBA { + sinfo, ok := svgs.Load(overriddenName(name, o)) if !ok || sinfo == nil { return nil } @@ -24,14 +26,14 @@ func GetSvg(name string, w int, h int) *image.NRGBA { } // SetSvg sets a svg into the cache map. -func SetSvg(name string, pix *image.NRGBA, w int, h int) { +func SetSvg(name string, o fyne.CanvasObject, pix *image.NRGBA, w int, h int) { sinfo := &svgInfo{ pix: pix, w: w, h: h, } sinfo.setAlive() - svgs.Store(name, sinfo) + svgs.Store(overriddenName(name, o), sinfo) } type svgInfo struct { @@ -57,3 +59,13 @@ func destroyExpiredSvgs(now time.Time) { svgs.Delete(exp) } } + +func overriddenName(name string, o fyne.CanvasObject) string { + if o != nil { // for overridden themes get the cache key right + if over, ok := overrides.Load(o); ok { + return over.(*overrideScope).cacheID + name + } + } + + return name +} diff --git a/internal/cache/svg_test.go b/internal/cache/svg_test.go index 3f0899e27b..5296ae0427 100644 --- a/internal/cache/svg_test.go +++ b/internal/cache/svg_test.go @@ -22,12 +22,12 @@ func TestSvgCacheGet(t *testing.T) { img := addToCache("empty.svg", "", 25, 25) assert.Equal(t, 1, syncMapLen(svgs)) - newImg := GetSvg("empty.svg", 25, 25) + newImg := GetSvg("empty.svg", nil, 25, 25) assert.Equal(t, img, newImg) - miss := GetSvg("missing.svg", 25, 25) + miss := GetSvg("missing.svg", nil, 25, 25) assert.Nil(t, miss) - miss = GetSvg("empty.svg", 30, 30) + miss = GetSvg("empty.svg", nil, 30, 30) assert.Nil(t, miss) } @@ -36,12 +36,12 @@ func TestSvgCacheGet_File(t *testing.T) { img := addFileToCache("testdata/stroke.svg", 25, 25) assert.Equal(t, 1, syncMapLen(svgs)) - newImg := GetSvg("testdata/stroke.svg", 25, 25) + newImg := GetSvg("testdata/stroke.svg", nil, 25, 25) assert.Equal(t, img, newImg) - miss := GetSvg("missing.svg", 25, 25) + miss := GetSvg("missing.svg", nil, 25, 25) assert.Nil(t, miss) - miss = GetSvg("testdata/stroke.svg", 30, 30) + miss = GetSvg("testdata/stroke.svg", nil, 30, 30) assert.Nil(t, miss) } @@ -56,13 +56,13 @@ func TestSvgCacheReset(t *testing.T) { func addFileToCache(path string, w, h int) image.Image { tex := image.NewNRGBA(image.Rect(0, 0, w, h)) - SetSvg(path, tex, w, h) + SetSvg(path, nil, tex, w, h) return tex } func addToCache(name, content string, w, h int) image.Image { resource := fyne.NewStaticResource(name, []byte(content)) tex := image.NewNRGBA(image.Rect(0, 0, w, h)) - SetSvg(resource.Name(), tex, w, h) + SetSvg(resource.Name(), nil, tex, w, h) return tex } diff --git a/internal/cache/text.go b/internal/cache/text.go index 465caec5bf..f41bb55b67 100644 --- a/internal/cache/text.go +++ b/internal/cache/text.go @@ -19,14 +19,19 @@ type fontMetric struct { } type fontSizeEntry struct { - text string - size float32 - style fyne.TextStyle + text string + size float32 + style fyne.TextStyle + custom string } // GetFontMetrics looks up a calculated size and baseline required for the specified text parameters. -func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle) (size fyne.Size, base float32) { - ent := fontSizeEntry{text, fontSize, style} +func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, base float32) { + name := "" + if source != nil { + name = source.Name() + } + ent := fontSizeEntry{text, fontSize, style, name} fontSizeLock.RLock() ret, ok := fontSizeCache[ent] fontSizeLock.RUnlock() @@ -38,8 +43,12 @@ func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle) (size f } // SetFontMetrics stores a calculated font size and baseline for parameters that were missing from the cache. -func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, size fyne.Size, base float32) { - ent := fontSizeEntry{text, fontSize, style} +func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource, size fyne.Size, base float32) { + name := "" + if source != nil { + name = source.Name() + } + ent := fontSizeEntry{text, fontSize, style, name} metric := &fontMetric{size: size, baseLine: base} metric.setAlive() fontSizeLock.Lock() diff --git a/internal/cache/text_test.go b/internal/cache/text_test.go index 4c2635fcf0..af727204fe 100644 --- a/internal/cache/text_test.go +++ b/internal/cache/text_test.go @@ -11,14 +11,14 @@ func TestTextCacheGet(t *testing.T) { ResetThemeCaches() assert.Equal(t, 0, len(fontSizeCache)) - bound, base := GetFontMetrics("hi", 10, fyne.TextStyle{}) + bound, base := GetFontMetrics("hi", 10, fyne.TextStyle{}, nil) assert.True(t, bound.IsZero()) assert.Equal(t, float32(0), base) - SetFontMetrics("hi", 10, fyne.TextStyle{}, fyne.NewSize(10, 10), 8) + SetFontMetrics("hi", 10, fyne.TextStyle{}, nil, fyne.NewSize(10, 10), 8) assert.Equal(t, 1, len(fontSizeCache)) - bound, base = GetFontMetrics("hi", 10, fyne.TextStyle{}) + bound, base = GetFontMetrics("hi", 10, fyne.TextStyle{}, nil) assert.Equal(t, fyne.NewSize(10, 10), bound) assert.Equal(t, float32(8), base) } diff --git a/internal/cache/theme.go b/internal/cache/theme.go index 0d1185647c..25efb597d1 100644 --- a/internal/cache/theme.go +++ b/internal/cache/theme.go @@ -6,7 +6,6 @@ import ( "sync/atomic" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/internal/svg" ) var ( @@ -30,50 +29,22 @@ func OverrideTheme(o fyne.CanvasObject, th fyne.Theme) { overrideTheme(o, s, id) } -func WidgetTheme(o fyne.Widget) fyne.Theme { +func WidgetScopeID(o fyne.CanvasObject) string { data, ok := overrides.Load(o) if !ok { - return nil + return "" } - return data.(*overrideScope).th + return data.(*overrideScope).cacheID } -func OverrideResourceTheme(res fyne.Resource, w fyne.Widget) fyne.Resource { - if th, ok := res.(fyne.ThemedResource); ok { - return &WidgetResource{ThemedResource: th, Owner: w} - } - - return res -} - -func themeForResource(res fyne.Resource) fyne.Theme { - if th, ok := res.(*WidgetResource); ok { - if over, ok := overrides.Load(th.Owner); ok { - return over.(*overrideScope).th - } +func WidgetTheme(o fyne.CanvasObject) fyne.Theme { + data, ok := overrides.Load(o) + if !ok { + return nil } - return fyne.CurrentApp().Settings().Theme() -} - -type WidgetResource struct { - fyne.ThemedResource - Owner fyne.Widget -} - -// Content returns the underlying content of the resource adapted to the current text color. -func (res *WidgetResource) Content() []byte { - th := themeForResource(res) - return svg.Colorize(res.ThemedResource.Content(), th.Color(res.ThemeColorName(), fyne.CurrentApp().Settings().ThemeVariant())) -} - -func (res *WidgetResource) Name() string { - cacheID := "" - if over, ok := overrides.Load(res.Owner); ok { - cacheID = over.(*overrideScope).cacheID - } - return cacheID + res.ThemedResource.Name() + return data.(*overrideScope).th } func overrideContainer(c *fyne.Container, s *overrideScope, id uint32) { @@ -88,6 +59,8 @@ func overrideTheme(o fyne.CanvasObject, s *overrideScope, id uint32) { overrideWidget(c, s, id) case *fyne.Container: overrideContainer(c, s, id) + default: + overrides.Store(c, s) } } diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 0dcaf756aa..de9a885365 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -72,8 +72,8 @@ func toOSIcon(icon []byte) ([]byte, error) { return buf.Bytes(), nil } -func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) { - return painter.RenderedTextSize(text, textSize, style) +func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + return painter.RenderedTextSize(text, textSize, style, source) } func (d *gLDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { @@ -166,6 +166,11 @@ func (d *gLDriver) Run() { go d.catchTerm() d.runGL() + + // Ensure lifecycle events run to completion before the app exits + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + l.WaitForEvents() + l.DestroyEventQueue() } func (d *gLDriver) DoubleTapDelay() time.Duration { diff --git a/internal/driver/glfw/driver_desktop.go b/internal/driver/glfw/driver_desktop.go index 5003b7319d..273b689249 100644 --- a/internal/driver/glfw/driver_desktop.go +++ b/internal/driver/glfw/driver_desktop.go @@ -86,15 +86,15 @@ func itemForMenuItem(i *fyne.MenuItem, parent *systray.MenuItem) *systray.MenuIt var item *systray.MenuItem if i.Checked { if parent != nil { - item = parent.AddSubMenuItemCheckbox(i.Label, i.Label, true) + item = parent.AddSubMenuItemCheckbox(i.Label, "", true) } else { - item = systray.AddMenuItemCheckbox(i.Label, i.Label, true) + item = systray.AddMenuItemCheckbox(i.Label, "", true) } } else { if parent != nil { - item = parent.AddSubMenuItem(i.Label, i.Label) + item = parent.AddSubMenuItem(i.Label, "") } else { - item = systray.AddMenuItem(i.Label, i.Label) + item = systray.AddMenuItem(i.Label, "") } } if i.Disabled { diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go index 6b1d8dc65b..e9fdc28b50 100644 --- a/internal/driver/glfw/loop.go +++ b/internal/driver/glfw/loop.go @@ -124,8 +124,9 @@ func (d *gLDriver) runGL() { eventTick.Stop() d.drawDone <- struct{}{} // wait for draw thread to stop d.Terminate() - if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStopped(); f != nil { - go f() // don't block main, we don't have window event queue + l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) } return case f := <-funcQueue: diff --git a/internal/driver/glfw/menu_darwin.go b/internal/driver/glfw/menu_darwin.go index 7b2629edbb..c6a4b8bc95 100644 --- a/internal/driver/glfw/menu_darwin.go +++ b/internal/driver/glfw/menu_darwin.go @@ -138,15 +138,19 @@ func exceptionCallback(e *C.char) { } func handleSpecialItems(w *window, menu *fyne.Menu, nextItemID int, addSeparator bool) (*fyne.Menu, int) { - for i, item := range menu.Items { - if item.Label == "Settings" || item.Label == "Settings…" || item.Label == "Preferences" || item.Label == "Preferences…" { + menu = fyne.NewMenu(menu.Label, menu.Items...) // copy so we can manipulate + for i := 0; i < len(menu.Items); i++ { + item := menu.Items[i] + switch item.Label { + case "About", "Settings", "Settings…", "Preferences", "Preferences…": items := make([]*fyne.MenuItem, 0, len(menu.Items)-1) items = append(items, menu.Items[:i]...) items = append(items, menu.Items[i+1:]...) menu, nextItemID = handleSpecialItems(w, fyne.NewMenu(menu.Label, items...), nextItemID, false) + i-- insertNativeMenuItem(C.darwinAppMenu(), item, nextItemID, 1) - if addSeparator { + if addSeparator && item.Label != "About" { C.insertDarwinMenuItem( C.darwinAppMenu(), C.CString(""), @@ -160,7 +164,6 @@ func handleSpecialItems(w *window, menu *fyne.Menu, nextItemID int, addSeparator ) } nextItemID = registerCallback(w, item, nextItemID) - break } } return menu, nextItemID diff --git a/internal/driver/glfw/menu_darwin.m b/internal/driver/glfw/menu_darwin.m index 458c5ab269..66157be086 100644 --- a/internal/driver/glfw/menu_darwin.m +++ b/internal/driver/glfw/menu_darwin.m @@ -83,10 +83,18 @@ void handleException(const char* m, id e) { exceptionCallback([[NSString stringWithFormat:@"%s failed: %@", m, e] UTF8String]); } -const void* insertDarwinMenuItem(const void* m, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int id, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength) { +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) { + item = [menu itemArray][0]; + [item setAction:@selector(tapped:)]; + [item setTarget:[FyneMenuHandler class]]; + [item setTag:nextId+menuTagMin]; + return item; + } + if (isSeparator) { item = [NSMenuItem separatorItem]; } else { @@ -98,7 +106,7 @@ void handleException(const char* m, id e) { [item setKeyEquivalentModifierMask: keyEquivalentModifierMask]; } [item setTarget:[FyneMenuHandler class]]; - [item setTag:id+menuTagMin]; + [item setTag:nextId+menuTagMin]; if (imageData) { char *x = (char *)imageData; NSData *data = [[NSData alloc] initWithBytes: imageData length: imageDataLength]; diff --git a/internal/driver/glfw/menu_darwin_test.go b/internal/driver/glfw/menu_darwin_test.go index de8d600534..df0dd3ffd4 100644 --- a/internal/driver/glfw/menu_darwin_test.go +++ b/internal/driver/glfw/menu_darwin_test.go @@ -3,6 +3,8 @@ package glfw import ( + "os" + "path/filepath" "testing" "unsafe" @@ -40,7 +42,8 @@ func TestDarwinMenu(t *testing.T) { itemRecent := fyne.NewMenuItem("Recent", nil) itemFoo := fyne.NewMenuItem("Foo", func() { lastAction = "foo" }) itemRecent.ChildMenu = fyne.NewMenu("", itemFoo) - menuEdit := fyne.NewMenu("File", itemNew, itemOpen, fyne.NewMenuItemSeparator(), itemRecent) + itemAbout := fyne.NewMenuItem("About", func() { lastAction = "about" }) + menuFile := fyne.NewMenu("File", itemNew, itemOpen, fyne.NewMenuItemSeparator(), itemRecent, itemAbout) itemHelp := fyne.NewMenuItem("Help", func() { lastAction = "Help!!!" }) itemHelp.Shortcut = &desktop.CustomShortcut{KeyName: fyne.KeyH, Modifier: fyne.KeyModifierControl} @@ -59,7 +62,7 @@ func TestDarwinMenu(t *testing.T) { itemMoreSetings := fyne.NewMenuItem("Settings…", func() { lastAction = "more settings" }) menuSettings := fyne.NewMenu("Settings", itemSettings, fyne.NewMenuItemSeparator(), itemMoreSetings) - mainMenu := fyne.NewMainMenu(menuEdit, menuHelp, menuMore, menuSettings) + mainMenu := fyne.NewMainMenu(menuFile, menuHelp, menuMore, menuSettings) runOnMain(func() { setupNativeMenu(w, mainMenu) }) @@ -70,7 +73,9 @@ func TestDarwinMenu(t *testing.T) { assert.Equal(t, 5, testNSMenuNumberOfItems(mm), "two built-in + three custom") m := testNSMenuItemSubmenu(testNSMenuItemAtIndex(mm, 0)) - assert.Equal(t, "", testNSMenuTitle(m), "app menu doesn’t have a title") + assert.Equal(t, "", testNSMenuTitle(m), "app menu doesn't have a title") + assertNSMenuItem(t, "About "+filepath.Base(os.Args[0]), "", 0, m, 0) + assertLastAction("about") assertNSMenuItemSeparator(m, 1) assertNSMenuItem(t, "Preferences", "", 0, m, 2) assertLastAction("prefs") diff --git a/internal/driver/glfw/testdata/menu_bar_hovered_content_test_theme.png b/internal/driver/glfw/testdata/menu_bar_hovered_content_test_theme.png index 54bf4635fb..2fc80a2090 100644 Binary files a/internal/driver/glfw/testdata/menu_bar_hovered_content_test_theme.png and b/internal/driver/glfw/testdata/menu_bar_hovered_content_test_theme.png differ diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_1.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_1.xml index ddc6ebd10c..785c2047d2 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_1.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_1.xml @@ -56,7 +56,7 @@ Recent - + @@ -85,7 +85,7 @@ Older - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_2.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_2.xml index e531e7009a..0a9fa42d92 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_2.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_close_submenu_2.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_1.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_1.xml index aa72779b20..9323324874 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_1.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_1.xml @@ -56,7 +56,7 @@ Recent - + @@ -85,7 +85,7 @@ Older - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_2.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_2.xml index 049ee467b8..71dde7c00b 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_2.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_open_submenu_2.xml @@ -56,7 +56,7 @@ Recent - + @@ -85,7 +85,7 @@ Older - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_left_3.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_left_3.xml index 187b9aee26..d5fbeec296 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_left_3.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_left_3.xml @@ -55,7 +55,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_right_3.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_right_3.xml index 187b9aee26..d5fbeec296 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_right_3.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_bar_items_right_3.xml @@ -55,7 +55,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_1.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_1.xml index 06219b15c8..48ecc96cfe 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_1.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_1.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_2.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_2.xml index 6c637652f3..03c6982d69 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_2.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_2.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_3.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_3.xml index e531e7009a..0a9fa42d92 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_3.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_down_3.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_1.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_1.xml index 6c637652f3..03c6982d69 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_1.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_1.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_2.xml b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_2.xml index 06219b15c8..48ecc96cfe 100644 --- a/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_2.xml +++ b/internal/driver/glfw/testdata/menu_bar_kbdctrl_traverse_menu_up_2.xml @@ -56,7 +56,7 @@ Recent - + diff --git a/internal/driver/glfw/testdata/windows_hover_object.xml b/internal/driver/glfw/testdata/windows_hover_object.xml index a265aa662e..6d5d0f8668 100644 --- a/internal/driver/glfw/testdata/windows_hover_object.xml +++ b/internal/driver/glfw/testdata/windows_hover_object.xml @@ -37,10 +37,10 @@ - + - + diff --git a/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml b/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml index 23b4048b83..9d6408dff6 100644 --- a/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml +++ b/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml @@ -36,10 +36,10 @@ - + - + diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 2605bee4f1..6f81a21532 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -227,7 +227,7 @@ func (w *window) Close() { func (w *window) ShowAndRun() { w.Show() - w.driver.Run() + fyne.CurrentApp().Run() } // Clipboard returns the system clipboard diff --git a/internal/driver/mobile/animation.go b/internal/driver/mobile/animation.go index a70504201d..aab26ddfc5 100644 --- a/internal/driver/mobile/animation.go +++ b/internal/driver/mobile/animation.go @@ -2,10 +2,10 @@ package mobile import "fyne.io/fyne/v2" -func (d *mobileDriver) StartAnimation(a *fyne.Animation) { +func (d *driver) StartAnimation(a *fyne.Animation) { d.animation.Start(a) } -func (d *mobileDriver) StopAnimation(a *fyne.Animation) { +func (d *driver) StopAnimation(a *fyne.Animation) { d.animation.Stop(a) } diff --git a/internal/driver/mobile/app/shiny.go b/internal/driver/mobile/app/shiny.go index 0ebf488dfb..b2c3afefc8 100644 --- a/internal/driver/mobile/app/shiny.go +++ b/internal/driver/mobile/app/shiny.go @@ -6,12 +6,10 @@ package app -import ( - "fmt" -) +import "log" func main(f func(a App)) { - fmt.Errorf("Running mobile simulation mode does not currently work on Windows.") + log.Fatalln("Running mobile simulation mode does not currently work on Windows.") } func GoBack() { diff --git a/internal/driver/mobile/canvas.go b/internal/driver/mobile/canvas.go index 65d3dc7bbb..5c8c6a5db1 100644 --- a/internal/driver/mobile/canvas.go +++ b/internal/driver/mobile/canvas.go @@ -10,163 +10,167 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/mobile" "fyne.io/fyne/v2/internal/app" - "fyne.io/fyne/v2/internal/driver" + intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) -var _ fyne.Canvas = (*mobileCanvas)(nil) +var _ fyne.Canvas = (*canvas)(nil) -type mobileCanvas struct { +type canvas struct { common.Canvas + content fyne.CanvasObject + device *device + initialized bool + lastTapDown map[int]time.Time + lastTapDownPos map[int]fyne.Position + menu fyne.CanvasObject + padded bool + scale float32 + size fyne.Size + touched map[int]mobile.Touchable + windowHead fyne.CanvasObject + + dragOffset fyne.Position + dragStart fyne.Position + dragging fyne.Draggable - content fyne.CanvasObject - windowHead, menu fyne.CanvasObject - scale float32 - size fyne.Size - - touched map[int]mobile.Touchable - padded bool - - onTypedRune func(rune) onTypedKey func(event *fyne.KeyEvent) + onTypedRune func(rune) - inited bool - lastTapDown map[int]time.Time - lastTapDownPos map[int]fyne.Position - dragging fyne.Draggable - dragStart, dragOffset fyne.Position - - touchTapCount int touchCancelFunc context.CancelFunc touchLastTapped fyne.CanvasObject + touchTapCount int } -// NewCanvas creates a new gomobile mobileCanvas. This is a mobileCanvas that will render on a mobile device using OpenGL. -func NewCanvas() fyne.Canvas { - ret := &mobileCanvas{padded: true} - ret.scale = fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile - ret.touched = make(map[int]mobile.Touchable) - ret.lastTapDownPos = make(map[int]fyne.Position) - ret.lastTapDown = make(map[int]time.Time) +func newCanvas(dev fyne.Device) fyne.Canvas { + d, _ := dev.(*device) + ret := &canvas{ + Canvas: common.Canvas{ + OnFocus: handleKeyboard, + OnUnfocus: hideVirtualKeyboard, + }, + device: d, + lastTapDown: make(map[int]time.Time), + lastTapDownPos: make(map[int]fyne.Position), + padded: true, + scale: dev.SystemScaleForWindow(nil), // we don't need a window parameter on mobile, + touched: make(map[int]mobile.Touchable), + } ret.Initialize(ret, ret.overlayChanged) - ret.OnFocus = ret.handleKeyboard - ret.OnUnfocus = hideVirtualKeyboard - return ret } -func (c *mobileCanvas) Capture() image.Image { +func (c *canvas) Capture() image.Image { return c.Painter().Capture(c) } -func (c *mobileCanvas) Content() fyne.CanvasObject { +func (c *canvas) Content() fyne.CanvasObject { return c.content } -func (c *mobileCanvas) InteractiveArea() (fyne.Position, fyne.Size) { - scale := fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile - - dev, ok := fyne.CurrentDevice().(*device) - if !ok { - return fyne.NewPos(0, 0), c.Size() // running in test mode +func (c *canvas) InteractiveArea() (fyne.Position, fyne.Size) { + var pos fyne.Position + var size fyne.Size + if c.device == nil { + // running in test mode + size = c.Size() + } else { + safeLeft := float32(c.device.safeLeft) / c.scale + safeTop := float32(c.device.safeTop) / c.scale + safeRight := float32(c.device.safeRight) / c.scale + safeBottom := float32(c.device.safeBottom) / c.scale + pos = fyne.NewPos(safeLeft, safeTop) + size = c.size.SubtractWidthHeight(safeLeft+safeRight, safeTop+safeBottom) + } + if c.windowHeadIsDisplacing() { + offset := c.windowHead.MinSize().Height + pos = pos.AddXY(0, offset) + size = size.SubtractWidthHeight(0, offset) } + return pos, size +} - safeLeft := float32(dev.safeLeft) / scale - safeTop := float32(dev.safeTop) / scale - safeRight := float32(dev.safeRight) / scale - safeBottom := float32(dev.safeBottom) / scale - return fyne.NewPos(safeLeft, safeTop), - c.size.SubtractWidthHeight(safeLeft+safeRight, safeTop+safeBottom) +func (c *canvas) MinSize() fyne.Size { + return c.size // TODO check } -func (c *mobileCanvas) OnTypedKey() func(*fyne.KeyEvent) { +func (c *canvas) OnTypedKey() func(*fyne.KeyEvent) { return c.onTypedKey } -func (c *mobileCanvas) OnTypedRune() func(rune) { +func (c *canvas) OnTypedRune() func(rune) { return c.onTypedRune } -func (c *mobileCanvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { +func (c *canvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { return int(float32(pos.X) * c.scale), int(float32(pos.Y) * c.scale) } -func (c *mobileCanvas) Scale() float32 { +func (c *canvas) Resize(size fyne.Size) { + if size == c.size { + return + } + + c.sizeContent(size) +} + +func (c *canvas) Scale() float32 { return c.scale } -func (c *mobileCanvas) SetContent(content fyne.CanvasObject) { +func (c *canvas) SetContent(content fyne.CanvasObject) { c.setContent(content) c.sizeContent(c.Size()) // fixed window size for mobile, cannot stretch to new content c.SetDirty() } -func (c *mobileCanvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { +func (c *canvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { c.onTypedKey = typed } -func (c *mobileCanvas) SetOnTypedRune(typed func(rune)) { +func (c *canvas) SetOnTypedRune(typed func(rune)) { c.onTypedRune = typed } -func (c *mobileCanvas) Size() fyne.Size { +func (c *canvas) Size() fyne.Size { return c.size } -func (c *mobileCanvas) MinSize() fyne.Size { - return c.size // TODO check -} - -func (c *mobileCanvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { +func (c *canvas) applyThemeOutOfTreeObjects() { if c.menu != nil { - return driver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.menu) + app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree + } + if c.windowHead != nil { + app.ApplyThemeTo(c.windowHead, c) // Ensure our child windows get the theme change message as it's out-of-tree } - - return driver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.windowHead, c.content) } -func (c *mobileCanvas) handleKeyboard(obj fyne.Focusable) { - isDisabled := false - if disWid, ok := obj.(fyne.Disableable); ok { - isDisabled = disWid.Disabled() - } - if obj != nil && !isDisabled { - if keyb, ok := obj.(mobile.Keyboardable); ok { - showVirtualKeyboard(keyb.Keyboard()) - } else { - showVirtualKeyboard(mobile.DefaultKeyboard) - } - } else { - hideVirtualKeyboard() +func (c *canvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { + if c.menu != nil { + return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.menu) } -} -func (c *mobileCanvas) overlayChanged() { - c.handleKeyboard(c.Focused()) - c.SetDirty() + return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.windowHead, c.content) } -func (c *mobileCanvas) Resize(size fyne.Size) { - if size == c.size { - return - } - - c.sizeContent(size) +func (c *canvas) overlayChanged() { + handleKeyboard(c.Focused()) + c.SetDirty() } -func (c *mobileCanvas) setContent(content fyne.CanvasObject) { +func (c *canvas) setContent(content fyne.CanvasObject) { c.content = content c.SetContentTreeAndFocusMgr(content) } -func (c *mobileCanvas) setMenu(menu fyne.CanvasObject) { +func (c *canvas) setMenu(menu fyne.CanvasObject) { c.menu = menu c.SetMenuTreeAndFocusMgr(menu) } -func (c *mobileCanvas) setWindowHead(head fyne.CanvasObject) { +func (c *canvas) setWindowHead(head fyne.CanvasObject) { if c.padded { head = container.NewPadded(head) } @@ -174,42 +178,27 @@ func (c *mobileCanvas) setWindowHead(head fyne.CanvasObject) { c.SetMobileWindowHeadTree(head) } -func (c *mobileCanvas) applyThemeOutOfTreeObjects() { - if c.menu != nil { - app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree - } - if c.windowHead != nil { - app.ApplyThemeTo(c.windowHead, c) // Ensure our child windows get the theme change message as it's out-of-tree - } -} - -func (c *mobileCanvas) sizeContent(size fyne.Size) { +func (c *canvas) sizeContent(size fyne.Size) { if c.content == nil { // window may not be configured yet return } - c.size = size - offset := fyne.NewPos(0, 0) + c.size = size areaPos, areaSize := c.InteractiveArea() if c.windowHead != nil { - topHeight := c.windowHead.MinSize().Height - - chromeBox := c.windowHead.(*fyne.Container) - if c.padded { - chromeBox = chromeBox.Objects[0].(*fyne.Container) // the padded container - } - if len(chromeBox.Objects) > 1 { - c.windowHead.Resize(fyne.NewSize(areaSize.Width, topHeight)) - offset = fyne.NewPos(0, topHeight) - areaSize = areaSize.Subtract(offset) + var headSize fyne.Size + headPos := areaPos + if c.windowHeadIsDisplacing() { + headSize = fyne.NewSize(areaSize.Width, c.windowHead.MinSize().Height) + headPos = headPos.SubtractXY(0, headSize.Height) } else { - c.windowHead.Resize(c.windowHead.MinSize()) + headSize = c.windowHead.MinSize() } - c.windowHead.Move(areaPos) + c.windowHead.Resize(headSize) + c.windowHead.Move(headPos) } - topLeft := areaPos.Add(offset) for _, overlay := range c.Overlays().List() { if p, ok := overlay.(*widget.PopUp); ok { // TODO: remove this when #707 is being addressed. @@ -217,20 +206,20 @@ func (c *mobileCanvas) sizeContent(size fyne.Size) { p.Refresh() } else { overlay.Resize(areaSize) - overlay.Move(topLeft) + overlay.Move(areaPos) } } if c.padded { c.content.Resize(areaSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2))) - c.content.Move(topLeft.Add(fyne.NewPos(theme.Padding(), theme.Padding()))) + c.content.Move(areaPos.Add(fyne.NewPos(theme.Padding(), theme.Padding()))) } else { c.content.Resize(areaSize) - c.content.Move(topLeft) + c.content.Move(areaPos) } } -func (c *mobileCanvas) tapDown(pos fyne.Position, tapID int) { +func (c *canvas) tapDown(pos fyne.Position, tapID int) { c.lastTapDown[tapID] = time.Now() c.lastTapDownPos[tapID] = pos c.dragging = nil @@ -259,7 +248,7 @@ func (c *mobileCanvas) tapDown(pos fyne.Position, tapID int) { } } -func (c *mobileCanvas) tapMove(pos fyne.Position, tapID int, +func (c *canvas) tapMove(pos fyne.Position, tapID int, dragCallback func(fyne.Draggable, *fyne.DragEvent)) { previousPos := c.lastTapDownPos[tapID] deltaX := pos.X - previousPos.X @@ -308,7 +297,7 @@ func (c *mobileCanvas) tapMove(pos fyne.Position, tapID int, dragCallback(c.dragging, ev) } -func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int, +func (c *canvas) tapUp(pos fyne.Position, tapID int, tapCallback func(fyne.Tappable, *fyne.PointEvent), tapAltCallback func(fyne.SecondaryTappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent), @@ -379,7 +368,7 @@ func (c *mobileCanvas) tapUp(pos fyne.Position, tapID int, } } -func (c *mobileCanvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) { +func (c *canvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) { var ctx context.Context ctx, c.touchCancelFunc = context.WithDeadline(context.TODO(), time.Now().Add(tapDoubleDelay)) defer c.touchCancelFunc() @@ -397,3 +386,15 @@ func (c *mobileCanvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEven c.touchCancelFunc = nil c.touchLastTapped = nil } + +func (c *canvas) windowHeadIsDisplacing() bool { + if c.windowHead == nil { + return false + } + + chromeBox := c.windowHead.(*fyne.Container) + if c.padded { + chromeBox = chromeBox.Objects[0].(*fyne.Container) // the padded container + } + return len(chromeBox.Objects) > 1 +} diff --git a/internal/driver/mobile/canvas_test.go b/internal/driver/mobile/canvas_test.go index 9e15882318..1e0d771dd4 100644 --- a/internal/driver/mobile/canvas_test.go +++ b/internal/driver/mobile/canvas_test.go @@ -9,7 +9,7 @@ import ( "time" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/mobile" _ "fyne.io/fyne/v2/test" @@ -27,16 +27,16 @@ func TestMain(m *testing.M) { os.Exit(ret) } -func TestCanvas_ChildMinSizeChangeAffectsAncestorsUpToRoot(t *testing.T) { - c := NewCanvas().(*mobileCanvas) - leftObj1 := canvas.NewRectangle(color.Black) +func Test_canvas_ChildMinSizeChangeAffectsAncestorsUpToRoot(t *testing.T) { + c := newCanvas(fyne.CurrentDevice()).(*canvas) + leftObj1 := fynecanvas.NewRectangle(color.Black) leftObj1.SetMinSize(fyne.NewSize(100, 50)) - leftObj2 := canvas.NewRectangle(color.Black) + leftObj2 := fynecanvas.NewRectangle(color.Black) leftObj2.SetMinSize(fyne.NewSize(100, 50)) leftCol := container.NewVBox(leftObj1, leftObj2) - rightObj1 := canvas.NewRectangle(color.Black) + rightObj1 := fynecanvas.NewRectangle(color.Black) rightObj1.SetMinSize(fyne.NewSize(100, 50)) - rightObj2 := canvas.NewRectangle(color.Black) + rightObj2 := fynecanvas.NewRectangle(color.Black) rightObj2.SetMinSize(fyne.NewSize(100, 50)) rightCol := container.NewVBox(rightObj1, rightObj2) content := container.NewHBox(leftCol, rightCol) @@ -53,118 +53,11 @@ func TestCanvas_ChildMinSizeChangeAffectsAncestorsUpToRoot(t *testing.T) { assert.Equal(t, expectedContentSize, content.Size()) } -func TestCanvas_PixelCoordinateAtPosition(t *testing.T) { - c := NewCanvas().(*mobileCanvas) - - pos := fyne.NewPos(4, 4) - c.scale = 2.5 - x, y := c.PixelCoordinateForPosition(pos) - assert.Equal(t, 10, x) - assert.Equal(t, 10, y) -} - -func TestCanvas_Tapped(t *testing.T) { - tapped := false - altTapped := false - buttonTap := false - var pointEvent *fyne.PointEvent - var tappedObj fyne.Tappable - button := widget.NewButton("Test", func() { - buttonTap = true - }) - c := NewCanvas().(*mobileCanvas) - c.SetContent(button) - c.Resize(fyne.NewSize(36, 24)) - button.Move(fyne.NewPos(3, 3)) - - tapPos := fyne.NewPos(6, 6) - c.tapDown(tapPos, 0) - c.tapUp(tapPos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { - tapped = true - tappedObj = wid - pointEvent = ev - wid.Tapped(ev) - }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { - altTapped = true - wid.TappedSecondary(ev) - }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { - wid.DoubleTapped(ev) - }, func(wid fyne.Draggable) { - }) - - assert.True(t, tapped, "tap primary") - assert.False(t, altTapped, "don't tap secondary") - assert.True(t, buttonTap, "button should be tapped") - assert.Equal(t, button, tappedObj) - if assert.NotNil(t, pointEvent) { - assert.Equal(t, fyne.NewPos(6, 6), pointEvent.AbsolutePosition) - assert.Equal(t, fyne.NewPos(3, 3), pointEvent.Position) - } -} - -func TestCanvas_Tapped_Multi(t *testing.T) { - buttonTap := false - button := widget.NewButton("Test", func() { - buttonTap = true - }) - c := NewCanvas().(*mobileCanvas) - c.SetContent(button) - c.Resize(fyne.NewSize(36, 24)) - button.Move(fyne.NewPos(3, 3)) - - tapPos := fyne.NewPos(6, 6) - c.tapDown(tapPos, 0) - c.tapUp(tapPos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) { // different tapID - wid.Tapped(ev) - }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { - }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { - wid.DoubleTapped(ev) - }, func(wid fyne.Draggable) { - }) - - assert.False(t, buttonTap, "button should not be tapped") -} - -func TestCanvas_TappedSecondary(t *testing.T) { - var pointEvent *fyne.PointEvent - var altTappedObj fyne.SecondaryTappable - obj := &tappableLabel{} - obj.ExtendBaseWidget(obj) - c := NewCanvas().(*mobileCanvas) - c.SetContent(obj) - c.Resize(fyne.NewSize(36, 24)) - obj.Move(fyne.NewPos(3, 3)) - - tapPos := fyne.NewPos(6, 6) - c.tapDown(tapPos, 0) - time.Sleep(310 * time.Millisecond) - c.tapUp(tapPos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { - obj.tap = true - wid.Tapped(ev) - }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { - obj.altTap = true - altTappedObj = wid - pointEvent = ev - wid.TappedSecondary(ev) - }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { - wid.DoubleTapped(ev) - }, func(wid fyne.Draggable) { - }) - - assert.False(t, obj.tap, "don't tap primary") - assert.True(t, obj.altTap, "tap secondary") - assert.Equal(t, obj, altTappedObj) - if assert.NotNil(t, pointEvent) { - assert.Equal(t, fyne.NewPos(6, 6), pointEvent.AbsolutePosition) - assert.Equal(t, fyne.NewPos(3, 3), pointEvent.Position) - } -} - -func TestCanvas_Dragged(t *testing.T) { +func Test_canvas_Dragged(t *testing.T) { dragged := false var draggedObj fyne.Draggable scroll := container.NewScroll(widget.NewLabel("Hi\nHi\nHi")) - c := NewCanvas().(*mobileCanvas) + c := newCanvas(fyne.CurrentDevice()).(*canvas) c.SetContent(scroll) c.Resize(fyne.NewSize(40, 24)) assert.Equal(t, float32(0), scroll.Offset.Y) @@ -187,8 +80,8 @@ func TestCanvas_Dragged(t *testing.T) { assert.Equal(t, fyne.NewPos(0, 5), scroll.Offset) } -func TestCanvas_DraggingOutOfWidget(t *testing.T) { - c := NewCanvas().(*mobileCanvas) +func Test_canvas_DraggingOutOfWidget(t *testing.T) { + c := newCanvas(fyne.CurrentDevice()).(*canvas) slider := widget.NewSlider(0.0, 100.0) c.SetContent(container.NewGridWithRows(2, slider, widget.NewLabel("Outside"))) c.Resize(fyne.NewSize(100, 200)) @@ -217,10 +110,135 @@ func TestCanvas_DraggingOutOfWidget(t *testing.T) { assert.Greater(t, slider.Value, lastValue) } -func TestCanvas_Tappable(t *testing.T) { +func Test_canvas_Focusable(t *testing.T) { + c := newCanvas(fyne.CurrentDevice()).(*canvas) + content := newFocusableEntry(c) + c.SetContent(content) + content.Resize(fyne.NewSize(25, 25)) + + pos := fyne.NewPos(10, 10) + c.tapDown(pos, 0) + c.tapUp(pos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { + wid.Tapped(ev) + }, nil, nil, nil) + time.Sleep(tapDoubleDelay + 150*time.Millisecond) + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 0, content.unfocusedTimes) + + c.tapDown(pos, 1) + c.tapUp(pos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) { + wid.Tapped(ev) + }, nil, nil, nil) + time.Sleep(tapDoubleDelay + 150*time.Millisecond) + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 0, content.unfocusedTimes) + + c.Focus(content) + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 0, content.unfocusedTimes) + + c.Unfocus() + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 1, content.unfocusedTimes) + + content.Disable() + c.Focus(content) + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 1, content.unfocusedTimes) + + c.tapDown(fyne.NewPos(10, 10), 2) + assert.Equal(t, 1, content.focusedTimes) + assert.Equal(t, 1, content.unfocusedTimes) +} + +func Test_canvas_InteractiveArea(t *testing.T) { + dev := &device{ + safeTop: 17, + safeLeft: 42, + safeBottom: 71, + safeRight: 24, + } + scale := dev.SystemScaleForWindow(nil) + + c := newCanvas(dev) + c.SetContent(fynecanvas.NewRectangle(color.Black)) + canvasSize := fyne.NewSize(600, 800) + c.(*canvas).Resize(canvasSize) + + t.Run("for canvas with size", func(t *testing.T) { + pos, size := c.InteractiveArea() + assert.Equal(t, fyne.NewPos(float32(dev.safeLeft)/scale, float32(dev.safeTop)/scale), pos) + assert.Equal(t, canvasSize.SubtractWidthHeight(float32(dev.safeLeft+dev.safeRight)/scale, float32(dev.safeTop+dev.safeBottom)/scale), size) + }) + + t.Run("when canvas size has changed", func(t *testing.T) { + changedCanvasSize := fyne.NewSize(800, 600) + c.(*canvas).Resize(changedCanvasSize) + pos, size := c.InteractiveArea() + assert.Equal(t, fyne.NewPos(float32(dev.safeLeft)/scale, float32(dev.safeTop)/scale), pos) + assert.Equal(t, changedCanvasSize.SubtractWidthHeight(float32(dev.safeLeft+dev.safeRight)/scale, float32(dev.safeTop+dev.safeBottom)/scale), size) + }) + + t.Run("when canvas got hovering window head", func(t *testing.T) { + c.(*canvas).Resize(canvasSize) + hoveringMenu := fynecanvas.NewRectangle(color.Black) + hoveringMenu.SetMinSize(fyne.NewSize(17, 17)) + windowHead := container.NewHBox(hoveringMenu) // a single object in window head is considered to be the hovering menu + c.(*canvas).setWindowHead(windowHead) + pos, size := c.InteractiveArea() + assert.Equal(t, fyne.NewPos(float32(dev.safeLeft)/scale, float32(dev.safeTop)/scale), pos) + assert.Equal(t, canvasSize.SubtractWidthHeight(float32(dev.safeLeft+dev.safeRight)/scale, float32(dev.safeTop+dev.safeBottom)/scale), size) + }) + + t.Run("when canvas got displacing window head", func(t *testing.T) { + c.(*canvas).Resize(canvasSize) + menu := fynecanvas.NewRectangle(color.Black) + menu.SetMinSize(fyne.NewSize(17, 17)) + title := fynecanvas.NewRectangle(color.Black) + title.SetMinSize(fyne.NewSize(42, 20)) + expectedOffset := 20 + 2*theme.Padding() + windowHead := container.NewHBox(menu, title) // two or more objects in window head are considered to be the window title bar + c.(*canvas).setWindowHead(windowHead) + pos, size := c.InteractiveArea() + assert.Equal(t, fyne.NewPos(float32(dev.safeLeft)/scale, float32(dev.safeTop)/scale+expectedOffset), pos) + assert.Equal(t, canvasSize.SubtractWidthHeight(float32(dev.safeLeft+dev.safeRight)/scale, float32(dev.safeTop+dev.safeBottom)/scale+expectedOffset), size) + }) +} + +func Test_canvas_PixelCoordinateAtPosition(t *testing.T) { + c := newCanvas(fyne.CurrentDevice()).(*canvas) + + pos := fyne.NewPos(4, 4) + c.scale = 2.5 + x, y := c.PixelCoordinateForPosition(pos) + assert.Equal(t, 10, x) + assert.Equal(t, 10, y) +} + +func Test_canvas_ResizeWithModalPopUpOverlay(t *testing.T) { + c := newCanvas(fyne.CurrentDevice()).(*canvas) + + c.SetContent(widget.NewLabel("Content")) + + popup := widget.NewModalPopUp(widget.NewLabel("PopUp"), c) + popupBgSize := fyne.NewSize(200, 200) + popup.Show() + popup.Resize(popupBgSize) + + canvasSize := fyne.NewSize(600, 700) + c.Resize(canvasSize) + + // get popup content padding dynamically + popupContentPadding := popup.MinSize().Subtract(popup.Content.MinSize()) + + assert.Equal(t, popupBgSize.Subtract(popupContentPadding), popup.Content.Size()) + assert.Equal(t, canvasSize, popup.Size()) +} + +func Test_canvas_Tappable(t *testing.T) { content := &touchableLabel{Label: widget.NewLabel("Hi\nHi\nHi")} content.ExtendBaseWidget(content) - c := NewCanvas().(*mobileCanvas) + c := newCanvas(fyne.CurrentDevice()).(*canvas) c.SetContent(content) c.Resize(fyne.NewSize(36, 24)) content.Resize(fyne.NewSize(24, 24)) @@ -242,7 +260,46 @@ func TestCanvas_Tappable(t *testing.T) { assert.True(t, content.cancel) } -func TestWindow_TappedAndDoubleTapped(t *testing.T) { +func Test_canvas_Tapped(t *testing.T) { + tapped := false + altTapped := false + buttonTap := false + var pointEvent *fyne.PointEvent + var tappedObj fyne.Tappable + button := widget.NewButton("Test", func() { + buttonTap = true + }) + c := newCanvas(fyne.CurrentDevice()).(*canvas) + c.SetContent(button) + c.Resize(fyne.NewSize(36, 24)) + button.Move(fyne.NewPos(3, 3)) + + tapPos := fyne.NewPos(6, 6) + c.tapDown(tapPos, 0) + c.tapUp(tapPos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { + tapped = true + tappedObj = wid + pointEvent = ev + wid.Tapped(ev) + }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { + altTapped = true + wid.TappedSecondary(ev) + }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { + wid.DoubleTapped(ev) + }, func(wid fyne.Draggable) { + }) + + assert.True(t, tapped, "tap primary") + assert.False(t, altTapped, "don't tap secondary") + assert.True(t, buttonTap, "button should be tapped") + assert.Equal(t, button, tappedObj) + if assert.NotNil(t, pointEvent) { + assert.Equal(t, fyne.NewPos(6, 6), pointEvent.AbsolutePosition) + assert.Equal(t, fyne.NewPos(3, 3), pointEvent.Position) + } +} + +func Test_canvas_TappedAndDoubleTapped(t *testing.T) { tapped := 0 but := newDoubleTappableButton() but.OnTapped = func() { @@ -252,7 +309,7 @@ func TestWindow_TappedAndDoubleTapped(t *testing.T) { tapped = 2 } - c := NewCanvas().(*mobileCanvas) + c := newCanvas(fyne.CurrentDevice()).(*canvas) c.SetContent(container.NewStack(but)) c.Resize(fyne.NewSize(36, 24)) @@ -266,65 +323,62 @@ func TestWindow_TappedAndDoubleTapped(t *testing.T) { assert.Equal(t, 2, tapped) } -func TestGlCanvas_ResizeWithModalPopUpOverlay(t *testing.T) { - c := NewCanvas().(*mobileCanvas) - - c.SetContent(widget.NewLabel("Content")) - - popup := widget.NewModalPopUp(widget.NewLabel("PopUp"), c) - popupBgSize := fyne.NewSize(200, 200) - popup.Show() - popup.Resize(popupBgSize) - - canvasSize := fyne.NewSize(600, 700) - c.Resize(canvasSize) +func Test_canvas_TappedMulti(t *testing.T) { + buttonTap := false + button := widget.NewButton("Test", func() { + buttonTap = true + }) + c := newCanvas(fyne.CurrentDevice()).(*canvas) + c.SetContent(button) + c.Resize(fyne.NewSize(36, 24)) + button.Move(fyne.NewPos(3, 3)) - // get popup content padding dynamically - popupContentPadding := popup.MinSize().Subtract(popup.Content.MinSize()) + tapPos := fyne.NewPos(6, 6) + c.tapDown(tapPos, 0) + c.tapUp(tapPos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) { // different tapID + wid.Tapped(ev) + }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { + }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { + wid.DoubleTapped(ev) + }, func(wid fyne.Draggable) { + }) - assert.Equal(t, popupBgSize.Subtract(popupContentPadding), popup.Content.Size()) - assert.Equal(t, canvasSize, popup.Size()) + assert.False(t, buttonTap, "button should not be tapped") } -func TestCanvas_Focusable(t *testing.T) { - c := NewCanvas().(*mobileCanvas) - content := newFocusableEntry(c) - c.SetContent(content) - content.Resize(fyne.NewSize(25, 25)) - - pos := fyne.NewPos(10, 10) - c.tapDown(pos, 0) - c.tapUp(pos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { - wid.Tapped(ev) - }, nil, nil, nil) - time.Sleep(tapDoubleDelay + 150*time.Millisecond) - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 0, content.unfocusedTimes) +func Test_canvas_TappedSecondary(t *testing.T) { + var pointEvent *fyne.PointEvent + var altTappedObj fyne.SecondaryTappable + obj := &tappableLabel{} + obj.ExtendBaseWidget(obj) + c := newCanvas(fyne.CurrentDevice()).(*canvas) + c.SetContent(obj) + c.Resize(fyne.NewSize(36, 24)) + obj.Move(fyne.NewPos(3, 3)) - c.tapDown(pos, 1) - c.tapUp(pos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) { + tapPos := fyne.NewPos(6, 6) + c.tapDown(tapPos, 0) + time.Sleep(310 * time.Millisecond) + c.tapUp(tapPos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { + obj.tap = true wid.Tapped(ev) - }, nil, nil, nil) - time.Sleep(tapDoubleDelay + 150*time.Millisecond) - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 0, content.unfocusedTimes) - - c.Focus(content) - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 0, content.unfocusedTimes) - - c.Unfocus() - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 1, content.unfocusedTimes) - - content.Disable() - c.Focus(content) - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 1, content.unfocusedTimes) + }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { + obj.altTap = true + altTappedObj = wid + pointEvent = ev + wid.TappedSecondary(ev) + }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { + wid.DoubleTapped(ev) + }, func(wid fyne.Draggable) { + }) - c.tapDown(fyne.NewPos(10, 10), 2) - assert.Equal(t, 1, content.focusedTimes) - assert.Equal(t, 1, content.unfocusedTimes) + assert.False(t, obj.tap, "don't tap primary") + assert.True(t, obj.altTap, "tap secondary") + assert.Equal(t, obj, altTappedObj) + if assert.NotNil(t, pointEvent) { + assert.Equal(t, fyne.NewPos(6, 6), pointEvent.AbsolutePosition) + assert.Equal(t, fyne.NewPos(3, 3), pointEvent.Position) + } } type touchableLabel struct { @@ -402,7 +456,7 @@ func newDoubleTappableButton() *doubleTappableButton { return but } -func simulateTap(c *mobileCanvas) { +func simulateTap(c *canvas) { c.tapDown(fyne.NewPos(15, 15), 0) time.Sleep(50 * time.Millisecond) c.tapUp(fyne.NewPos(15, 15), 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 565aa9fdeb..9d8e5c3c75 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -7,14 +7,14 @@ import ( "time" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/driver/mobile" "fyne.io/fyne/v2/internal" "fyne.io/fyne/v2/internal/animation" intapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/internal/cache" - "fyne.io/fyne/v2/internal/driver" + intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/internal/driver/mobile/app" "fyne.io/fyne/v2/internal/driver/mobile/event/key" @@ -45,7 +45,7 @@ type ConfiguredDriver interface { SetOnConfigurationChanged(func(*Configuration)) } -type mobileDriver struct { +type driver struct { app app.App glctx gl.Context @@ -61,30 +61,30 @@ type mobileDriver struct { } // Declare conformity with Driver -var _ fyne.Driver = (*mobileDriver)(nil) -var _ ConfiguredDriver = (*mobileDriver)(nil) +var _ fyne.Driver = (*driver)(nil) +var _ ConfiguredDriver = (*driver)(nil) func init() { runtime.LockOSThread() } -func (d *mobileDriver) CreateWindow(title string) fyne.Window { - c := NewCanvas().(*mobileCanvas) // silence lint +func (d *driver) CreateWindow(title string) fyne.Window { + c := newCanvas(fyne.CurrentDevice()).(*canvas) // silence lint ret := &window{title: title, canvas: c, isChild: len(d.windows) > 0} ret.InitEventQueue() go ret.RunEventQueue() - c.setContent(&canvas.Rectangle{FillColor: theme.BackgroundColor()}) + c.setContent(&fynecanvas.Rectangle{FillColor: theme.BackgroundColor()}) c.SetPainter(pgl.NewPainter(c, ret)) d.windows = append(d.windows, ret) return ret } -func (d *mobileDriver) AllWindows() []fyne.Window { +func (d *driver) AllWindows() []fyne.Window { return d.windows } // currentWindow returns the most recently opened window - we can only show one at a time. -func (d *mobileDriver) currentWindow() *window { +func (d *driver) currentWindow() *window { if len(d.windows) == 0 { return nil } @@ -100,11 +100,11 @@ func (d *mobileDriver) currentWindow() *window { return last } -func (d *mobileDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) { - return painter.RenderedTextSize(text, textSize, style) +func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + return painter.RenderedTextSize(text, textSize, style, source) } -func (d *mobileDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { +func (d *driver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { if len(d.windows) == 0 { return nil } @@ -113,34 +113,27 @@ func (d *mobileDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { return d.currentWindow().Canvas() } -func (d *mobileDriver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { +func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { c := d.CanvasForObject(co) if c == nil { return fyne.NewPos(0, 0) } - mc := c.(*mobileCanvas) - pos := driver.AbsolutePositionForObject(co, mc.ObjectTrees()) + mc := c.(*canvas) + pos := intdriver.AbsolutePositionForObject(co, mc.ObjectTrees()) inset, _ := c.InteractiveArea() - - if mc.windowHead != nil { - if len(mc.windowHead.(*fyne.Container).Objects) > 1 { - topHeight := mc.windowHead.MinSize().Height - pos = pos.Subtract(fyne.NewSize(0, topHeight)) - } - } return pos.Subtract(inset) } -func (d *mobileDriver) GoBack() { +func (d *driver) GoBack() { app.GoBack() } -func (d *mobileDriver) Quit() { +func (d *driver) Quit() { // Android and iOS guidelines say this should not be allowed! } -func (d *mobileDriver) Run() { +func (d *driver) Run() { if !d.running.CompareAndSwap(false, true) { return // Run was called twice. } @@ -150,6 +143,11 @@ func (d *mobileDriver) Run() { settingsChange := make(chan fyne.Settings) fyne.CurrentApp().Settings().AddChangeListener(settingsChange) draw := time.NewTicker(time.Second / 60) + defer func() { + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + l.WaitForEvents() + l.DestroyEventQueue() + }() for { select { @@ -159,7 +157,7 @@ func (d *mobileDriver) Run() { painter.ClearFontCache() cache.ResetThemeCaches() intapp.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) { - c, ok := w.Canvas().(*mobileCanvas) + c, ok := w.Canvas().(*canvas) if !ok { return } @@ -173,7 +171,7 @@ func (d *mobileDriver) Run() { if current == nil { continue } - c := current.Canvas().(*mobileCanvas) + c := current.Canvas().(*canvas) switch e := a.Filter(e).(type) { case lifecycle.Event: @@ -225,12 +223,12 @@ func (d *mobileDriver) Run() { }) } -func (*mobileDriver) SetDisableScreenBlanking(disable bool) { +func (*driver) SetDisableScreenBlanking(disable bool) { setDisableScreenBlank(disable) } -func (d *mobileDriver) handleLifecycle(e lifecycle.Event, w *window) { - c := w.Canvas().(*mobileCanvas) +func (d *driver) handleLifecycle(e lifecycle.Event, w *window) { + c := w.Canvas().(*canvas) switch e.Crosses(lifecycle.StageVisible) { case lifecycle.CrossOn: d.glctx, _ = e.DrawContext.(gl.Context) @@ -265,14 +263,14 @@ func (d *mobileDriver) handleLifecycle(e lifecycle.Event, w *window) { } } -func (d *mobileDriver) handlePaint(e paint.Event, w fyne.Window) { - c := w.Canvas().(*mobileCanvas) +func (d *driver) handlePaint(e paint.Event, w fyne.Window) { + c := w.Canvas().(*canvas) d.painting = false if d.glctx == nil || e.External { return } - if !c.inited { - c.inited = true + if !c.initialized { + c.initialized = true c.Painter().Init() // we cannot init until the context is set above } @@ -292,21 +290,22 @@ func (d *mobileDriver) handlePaint(e paint.Event, w fyne.Window) { cache.Clean(canvasNeedRefresh) } -func (d *mobileDriver) onStart() { +func (d *driver) onStart() { if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnStarted(); f != nil { go f() // don't block main, we don't have window event queue } } -func (d *mobileDriver) onStop() { - if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnStopped(); f != nil { - go f() // don't block main, we don't have window event queue +func (d *driver) onStop() { + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) } } -func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) { +func (d *driver) paintWindow(window fyne.Window, size fyne.Size) { clips := &internal.ClipStack{} - c := window.Canvas().(*mobileCanvas) + c := window.Canvas().(*canvas) r, g, b, a := theme.BackgroundColor().RGBA() max16bit := float32(255 * 255) @@ -342,7 +341,7 @@ func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) { c.WalkTrees(draw, afterDraw) } -func (d *mobileDriver) sendPaintEvent() { +func (d *driver) sendPaintEvent() { if d.painting { return } @@ -350,7 +349,7 @@ func (d *mobileDriver) sendPaintEvent() { d.painting = true } -func (d *mobileDriver) setTheme(dark bool) { +func (d *driver) setTheme(dark bool) { var mode fyne.ThemeVariant if dark { mode = theme.VariantDark @@ -364,7 +363,7 @@ func (d *mobileDriver) setTheme(dark bool) { d.theme = mode } -func (d *mobileDriver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequence) { +func (d *driver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) @@ -372,7 +371,7 @@ func (d *mobileDriver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequen w.canvas.tapDown(pos, int(tapID)) } -func (d *mobileDriver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) { +func (d *driver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) @@ -382,7 +381,7 @@ func (d *mobileDriver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequen }) } -func (d *mobileDriver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { +func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { tapX := scale.ToFyneCoordinate(w.canvas, int(x)) tapY := scale.ToFyneCoordinate(w.canvas, int(y)) pos := fyne.NewPos(tapX, tapY+tapYOffset) @@ -509,7 +508,7 @@ func runeToPrintable(r rune) rune { return 0 } -func (d *mobileDriver) typeDownCanvas(canvas *mobileCanvas, r rune, code key.Code, mod key.Modifiers) { +func (d *driver) typeDownCanvas(canvas *canvas, r rune, code key.Code, mod key.Modifiers) { keyName := keyToName(code) switch keyName { case fyne.KeyTab: @@ -553,25 +552,25 @@ func (d *mobileDriver) typeDownCanvas(canvas *mobileCanvas, r rune, code key.Cod } } -func (d *mobileDriver) typeUpCanvas(_ *mobileCanvas, _ rune, _ key.Code, _ key.Modifiers) { +func (d *driver) typeUpCanvas(_ *canvas, _ rune, _ key.Code, _ key.Modifiers) { } -func (d *mobileDriver) Device() fyne.Device { +func (d *driver) Device() fyne.Device { return &d.device } -func (d *mobileDriver) SetOnConfigurationChanged(f func(*Configuration)) { +func (d *driver) SetOnConfigurationChanged(f func(*Configuration)) { d.onConfigChanged = f } -func (d *mobileDriver) DoubleTapDelay() time.Duration { +func (d *driver) DoubleTapDelay() time.Duration { return tapDoubleDelay } // NewGoMobileDriver sets up a new Driver instance implemented using the Go // Mobile extension and OpenGL bindings. func NewGoMobileDriver() fyne.Driver { - d := &mobileDriver{ + d := &driver{ theme: fyne.ThemeVariant(2), // unspecified } diff --git a/internal/driver/mobile/driver_android.go b/internal/driver/mobile/driver_android.go index 9068e312f5..da28d4c60c 100644 --- a/internal/driver/mobile/driver_android.go +++ b/internal/driver/mobile/driver_android.go @@ -2,7 +2,7 @@ package mobile -import "fyne.io/fyne/v2/driver" +import driverDefs "fyne.io/fyne/v2/driver" /* #include @@ -13,8 +13,8 @@ void keepScreenOn(uintptr_t jni_env, uintptr_t ctx, bool disabled); import "C" func setDisableScreenBlank(disable bool) { - driver.RunNative(func(ctx any) error { - ac := ctx.(*driver.AndroidContext) + driverDefs.RunNative(func(ctx any) error { + ac := ctx.(*driverDefs.AndroidContext) C.keepScreenOn(C.uintptr_t(ac.Env), C.uintptr_t(ac.Ctx), C.bool(disable)) diff --git a/internal/driver/mobile/driver_test.go b/internal/driver/mobile/driver_test.go new file mode 100644 index 0000000000..a0eb37d48f --- /dev/null +++ b/internal/driver/mobile/driver_test.go @@ -0,0 +1,82 @@ +package mobile + +import ( + "image/color" + "testing" + + "github.com/stretchr/testify/assert" + + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +func Test_mobileDriver_AbsolutePositionForObject(t *testing.T) { + for name, tt := range map[string]struct { + want fyne.Position + windowIsChild bool + windowPadded bool + }{ + "for an unpadded primary (non-child) window it is (0,0)": { + want: fyne.NewPos(0, 0), + windowIsChild: false, + windowPadded: false, + }, + "for a padded primary (non-child) window it is (padding,padding)": { + want: fyne.NewPos(4, 4), + windowIsChild: false, + windowPadded: true, + }, + "for an unpadded child window it is (0,0)": { + want: fyne.NewPos(0, 0), + windowIsChild: true, + windowPadded: false, + }, + "for a padded child window it is (padding,padding)": { + want: fyne.NewPos(4, 4), + windowIsChild: true, + windowPadded: true, + }, + } { + t.Run(name, func(t *testing.T) { + var o fyne.CanvasObject + size := fyne.NewSize(100, 100) + d := &driver{} + w := d.CreateWindow("main") + w.SetPadded(tt.windowPadded) + l := widget.NewLabel("main window") + if !tt.windowIsChild { + o = l + } + w.SetContent(l) + w.Show() + w.Resize(size) + w = d.CreateWindow("child1") + w.SetContent(widget.NewLabel("first child")) + if tt.windowIsChild { + w.Show() + } + w.Resize(size) + w = d.CreateWindow("child2 - hidden") + w.SetContent(widget.NewLabel("second child")) + w.Resize(size) + w = d.CreateWindow("child3") + r := fynecanvas.NewRectangle(color.White) + r.SetMinSize(fyne.NewSize(42, 17)) + w.SetPadded(tt.windowPadded) + w.SetContent(container.NewVBox(r)) + if tt.windowIsChild { + w.Show() + o = r + } + w.Resize(size) + w = d.CreateWindow("child4 - hidden") + w.SetContent(widget.NewLabel("fourth child")) + w.Resize(size) + + got := d.AbsolutePositionForObject(o) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/driver/mobile/file.go b/internal/driver/mobile/file.go index 03d9b69095..d3af9b0e57 100644 --- a/internal/driver/mobile/file.go +++ b/internal/driver/mobile/file.go @@ -48,7 +48,7 @@ type hasOpenPicker interface { // ShowFileOpenPicker loads the native file open dialog and returns the chosen file path via the callback func. func ShowFileOpenPicker(callback func(fyne.URIReadCloser, error), filter storage.FileFilter) { - drv := fyne.CurrentApp().Driver().(*mobileDriver) + drv := fyne.CurrentApp().Driver().(*driver) if a, ok := drv.app.(hasOpenPicker); ok { a.ShowFileOpenPicker(func(uri string, closer func()) { if uri == "" { @@ -67,7 +67,7 @@ func ShowFileOpenPicker(callback func(fyne.URIReadCloser, error), filter storage // ShowFolderOpenPicker loads the native folder open dialog and calls back the chosen directory path as a ListableURI. func ShowFolderOpenPicker(callback func(fyne.ListableURI, error)) { filter := storage.NewMimeTypeFileFilter([]string{"application/x-directory"}) - drv := fyne.CurrentApp().Driver().(*mobileDriver) + drv := fyne.CurrentApp().Driver().(*driver) if a, ok := drv.app.(hasOpenPicker); ok { a.ShowFileOpenPicker(func(path string, _ func()) { if path == "" { @@ -112,7 +112,7 @@ type hasSavePicker interface { // ShowFileSavePicker loads the native file save dialog and returns the chosen file path via the callback func. func ShowFileSavePicker(callback func(fyne.URIWriteCloser, error), filter storage.FileFilter, filename string) { - drv := fyne.CurrentApp().Driver().(*mobileDriver) + drv := fyne.CurrentApp().Driver().(*driver) if a, ok := drv.app.(hasSavePicker); ok { a.ShowFileSavePicker(func(path string, closer func()) { if path == "" { diff --git a/internal/driver/mobile/file_android.go b/internal/driver/mobile/file_android.go index 9bb7bb8ee8..b266c92916 100644 --- a/internal/driver/mobile/file_android.go +++ b/internal/driver/mobile/file_android.go @@ -171,7 +171,7 @@ func existsURI(uri fyne.URI) (bool, error) { return ok, nil } -func registerRepository(d *mobileDriver) { +func registerRepository(d *driver) { repo := &mobileFileRepo{} repository.Register("file", repo) repository.Register("content", repo) diff --git a/internal/driver/mobile/file_desktop.go b/internal/driver/mobile/file_desktop.go index dc878eb0f2..8334d9d8e7 100644 --- a/internal/driver/mobile/file_desktop.go +++ b/internal/driver/mobile/file_desktop.go @@ -30,6 +30,6 @@ func nativeFileSave(*fileSave) (io.WriteCloser, error) { return nil, nil } -func registerRepository(d *mobileDriver) { +func registerRepository(d *driver) { repository.Register("file", intRepo.NewFileRepository()) } diff --git a/internal/driver/mobile/file_ios.go b/internal/driver/mobile/file_ios.go index b13a246486..10e0112b9d 100644 --- a/internal/driver/mobile/file_ios.go +++ b/internal/driver/mobile/file_ios.go @@ -151,7 +151,7 @@ func nativeFileSave(f *fileSave) (io.WriteCloser, error) { return fileStruct, nil } -func registerRepository(d *mobileDriver) { +func registerRepository(d *driver) { repo := &mobileFileRepo{} repository.Register("file", repo) } diff --git a/internal/driver/mobile/folder.go b/internal/driver/mobile/folder.go index ee073003ea..4e5dba04da 100644 --- a/internal/driver/mobile/folder.go +++ b/internal/driver/mobile/folder.go @@ -1,7 +1,7 @@ package mobile import ( - "fmt" + "errors" "fyne.io/fyne/v2" ) @@ -16,7 +16,7 @@ func (l *lister) List() ([]fyne.URI, error) { func listerForURI(uri fyne.URI) (fyne.ListableURI, error) { if !canListURI(uri) { - return nil, fmt.Errorf("specified URI is not listable") + return nil, errors.New("specified URI is not listable") } return &lister{uri}, nil diff --git a/internal/driver/mobile/keyboard.go b/internal/driver/mobile/keyboard.go index c3dd0d8cd1..3246a6669f 100644 --- a/internal/driver/mobile/keyboard.go +++ b/internal/driver/mobile/keyboard.go @@ -6,23 +6,39 @@ import ( "fyne.io/fyne/v2/internal/driver/mobile/app" ) -func showVirtualKeyboard(keyboard mobile.KeyboardType) { - if driver, ok := fyne.CurrentApp().Driver().(*mobileDriver); ok { - if driver.app == nil { // not yet running - fyne.LogError("Cannot show keyboard before app is running", nil) +func hideVirtualKeyboard() { + if d, ok := fyne.CurrentApp().Driver().(*driver); ok { + if d.app == nil { // not yet running return } - driver.app.ShowVirtualKeyboard(app.KeyboardType(keyboard)) + d.app.HideVirtualKeyboard() } } -func hideVirtualKeyboard() { - if driver, ok := fyne.CurrentApp().Driver().(*mobileDriver); ok { - if driver.app == nil { // not yet running +func handleKeyboard(obj fyne.Focusable) { + isDisabled := false + if disWid, ok := obj.(fyne.Disableable); ok { + isDisabled = disWid.Disabled() + } + if obj != nil && !isDisabled { + if keyb, ok := obj.(mobile.Keyboardable); ok { + showVirtualKeyboard(keyb.Keyboard()) + } else { + showVirtualKeyboard(mobile.DefaultKeyboard) + } + } else { + hideVirtualKeyboard() + } +} + +func showVirtualKeyboard(keyboard mobile.KeyboardType) { + if d, ok := fyne.CurrentApp().Driver().(*driver); ok { + if d.app == nil { // not yet running + fyne.LogError("Cannot show keyboard before app is running", nil) return } - driver.app.HideVirtualKeyboard() + d.app.ShowVirtualKeyboard(app.KeyboardType(keyboard)) } } diff --git a/internal/driver/mobile/menu.go b/internal/driver/mobile/menu.go index 43123baccc..3ca0b604d1 100644 --- a/internal/driver/mobile/menu.go +++ b/internal/driver/mobile/menu.go @@ -4,7 +4,7 @@ import ( "image/color" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" @@ -16,7 +16,7 @@ type menuLabel struct { menu *fyne.Menu bar *fyne.Container - canvas *mobileCanvas + canvas *canvas } func (m *menuLabel) Tapped(*fyne.PointEvent) { @@ -39,13 +39,13 @@ func (m *menuLabel) CreateRenderer() fyne.WidgetRenderer { return &menuLabelRenderer{menu: m, content: box} } -func newMenuLabel(item *fyne.Menu, parent *fyne.Container, c *mobileCanvas) *menuLabel { +func newMenuLabel(item *fyne.Menu, parent *fyne.Container, c *canvas) *menuLabel { l := &menuLabel{menu: item, bar: parent, canvas: c} l.ExtendBaseWidget(l) return l } -func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) { +func (c *canvas) showMenu(menu *fyne.MainMenu) { var panel *fyne.Container top := container.NewHBox(widget.NewButtonWithIcon("", theme.CancelIcon(), func() { panel.Hide() @@ -59,8 +59,8 @@ func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) { panel = container.NewPadded(panel) } - bg := canvas.NewRectangle(theme.BackgroundColor()) - shadow := canvas.NewHorizontalGradient(theme.ShadowColor(), color.Transparent) + bg := fynecanvas.NewRectangle(theme.BackgroundColor()) + shadow := fynecanvas.NewHorizontalGradient(theme.ShadowColor(), color.Transparent) safePos, safeSize := c.InteractiveArea() bg.Move(safePos) @@ -73,7 +73,7 @@ func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) { c.setMenu(container.NewWithoutLayout(bg, panel, shadow)) } -func (d *mobileDriver) findMenu(win *window) *fyne.MainMenu { +func (d *driver) findMenu(win *window) *fyne.MainMenu { if win.menu != nil { return win.menu } diff --git a/internal/driver/mobile/menu_test.go b/internal/driver/mobile/menu_test.go index 6bfafcb242..3b2527b98d 100644 --- a/internal/driver/mobile/menu_test.go +++ b/internal/driver/mobile/menu_test.go @@ -6,7 +6,7 @@ import ( "testing" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" internalWidget "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" @@ -15,8 +15,8 @@ import ( ) func TestMobileCanvas_DismissBar(t *testing.T) { - c := NewCanvas().(*mobileCanvas) - c.SetContent(canvas.NewRectangle(theme.BackgroundColor())) + c := newCanvas(fyne.CurrentDevice()).(*canvas) + c.SetContent(fynecanvas.NewRectangle(theme.BackgroundColor())) menu := fyne.NewMainMenu( fyne.NewMenu("Test")) c.showMenu(menu) @@ -30,9 +30,9 @@ func TestMobileCanvas_DismissBar(t *testing.T) { } func TestMobileCanvas_DismissMenu(t *testing.T) { - c := NewCanvas().(*mobileCanvas) + c := newCanvas(fyne.CurrentDevice()).(*canvas) c.padded = false - c.SetContent(canvas.NewRectangle(theme.BackgroundColor())) + c.SetContent(fynecanvas.NewRectangle(theme.BackgroundColor())) menu := fyne.NewMainMenu( fyne.NewMenu("Test", fyne.NewMenuItem("TapMe", func() {}))) c.showMenu(menu) @@ -49,7 +49,7 @@ func TestMobileCanvas_DismissMenu(t *testing.T) { } func TestMobileCanvas_Menu(t *testing.T) { - c := &mobileCanvas{} + c := &canvas{} labels := []string{"File", "Edit"} menu := fyne.NewMainMenu( fyne.NewMenu(labels[0]), @@ -71,7 +71,7 @@ func TestMobileCanvas_Menu(t *testing.T) { } } -func dummyWin(d *mobileDriver, title string) *window { +func dummyWin(d *driver, title string) *window { ret := &window{title: title} d.windows = append(d.windows, ret) @@ -82,7 +82,7 @@ func TestMobileDriver_FindMenu(t *testing.T) { m1 := fyne.NewMainMenu(fyne.NewMenu("1")) m2 := fyne.NewMainMenu(fyne.NewMenu("2")) - d := NewGoMobileDriver().(*mobileDriver) + d := NewGoMobileDriver().(*driver) w1 := dummyWin(d, "top") w1.SetMainMenu(m1) assert.Equal(t, m1, d.findMenu(w1)) diff --git a/internal/driver/mobile/menubutton.go b/internal/driver/mobile/menubutton.go index e937c22151..2eb8ec9101 100644 --- a/internal/driver/mobile/menubutton.go +++ b/internal/driver/mobile/menubutton.go @@ -2,7 +2,7 @@ package mobile import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -22,12 +22,12 @@ func (w *window) newMenuButton(menu *fyne.MainMenu) *menuButton { func (m *menuButton) CreateRenderer() fyne.WidgetRenderer { return &menuButtonRenderer{btn: widget.NewButtonWithIcon("", theme.MenuIcon(), func() { m.win.canvas.showMenu(m.menu) - }), bg: canvas.NewRectangle(theme.BackgroundColor())} + }), bg: fynecanvas.NewRectangle(theme.BackgroundColor())} } type menuButtonRenderer struct { btn *widget.Button - bg *canvas.Rectangle + bg *fynecanvas.Rectangle } func (m *menuButtonRenderer) Destroy() { diff --git a/internal/driver/mobile/window.go b/internal/driver/mobile/window.go index 3c044b9768..1a45acd30e 100644 --- a/internal/driver/mobile/window.go +++ b/internal/driver/mobile/window.go @@ -20,7 +20,7 @@ type window struct { isChild bool clipboard fyne.Clipboard - canvas *mobileCanvas + canvas *canvas icon fyne.Resource menu *fyne.MainMenu } @@ -42,7 +42,7 @@ func (w *window) SetFullScreen(bool) { } func (w *window) Resize(size fyne.Size) { - w.Canvas().(*mobileCanvas).Resize(size) + w.Canvas().(*canvas).Resize(size) } func (w *window) RequestFocus() { @@ -106,7 +106,7 @@ func (w *window) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) { } func (w *window) Show() { - menu := fyne.CurrentApp().Driver().(*mobileDriver).findMenu(w) + menu := fyne.CurrentApp().Driver().(*driver).findMenu(w) menuButton := w.newMenuButton(menu) if menu == nil { menuButton.Hide() @@ -150,7 +150,7 @@ func (w *window) tryClose() { } func (w *window) Close() { - d := fyne.CurrentApp().Driver().(*mobileDriver) + d := fyne.CurrentApp().Driver().(*driver) pos := -1 for i, win := range d.windows { if win == w { @@ -186,7 +186,7 @@ func (w *window) Close() { func (w *window) ShowAndRun() { w.Show() - fyne.CurrentApp().Driver().Run() + fyne.CurrentApp().Run() } func (w *window) Content() fyne.CanvasObject { @@ -219,5 +219,5 @@ func (w *window) RescaleContext() { } func (w *window) Context() any { - return fyne.CurrentApp().Driver().(*mobileDriver).glctx + return fyne.CurrentApp().Driver().(*driver).glctx } diff --git a/internal/painter/font.go b/internal/painter/font.go index 9ce954b9c7..77e795f5d5 100644 --- a/internal/painter/font.go +++ b/internal/painter/font.go @@ -11,11 +11,15 @@ import ( "github.com/go-text/render" "github.com/go-text/typesetting/di" "github.com/go-text/typesetting/font" + "github.com/go-text/typesetting/fontscan" + "github.com/go-text/typesetting/language" + "github.com/go-text/typesetting/opentype/api/metadata" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/theme" ) @@ -26,43 +30,130 @@ const ( fontTabSpaceSize = 10 ) +var ( + fm *fontscan.FontMap + mapLock = sync.Mutex{} + load sync.Once +) + +func loadMap() { + fm = fontscan.NewFontMap(noopLogger{}) + err := loadSystemFonts(fm) + if err != nil { + fm = nil // just don't fallback + } +} + +func lookupLangFont(family string, aspect metadata.Aspect) font.Face { + mapLock.Lock() + defer mapLock.Unlock() + load.Do(loadMap) + if fm == nil { + return nil + } + + fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect}) + l, _ := fontscan.NewLangID(language.Language(lang.SystemLocale().LanguageString())) + return fm.ResolveFaceForLang(l) +} + +func lookupRuneFont(r rune, family string, aspect metadata.Aspect) font.Face { + mapLock.Lock() + defer mapLock.Unlock() + load.Do(loadMap) + if fm == nil { + return nil + } + + fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect}) + return fm.ResolveFace(r) +} + +func lookupFaces(theme, fallback fyne.Resource, family string, style fyne.TextStyle) (faces *dynamicFontMap) { + f1 := loadMeasureFont(theme) + if theme == fallback { + faces = &dynamicFontMap{family: family, faces: []font.Face{f1}} + } else { + f2 := loadMeasureFont(fallback) + faces = &dynamicFontMap{family: family, faces: []font.Face{f1, f2}} + } + + aspect := metadata.Aspect{Style: metadata.StyleNormal} + if style.Italic { + aspect.Style = metadata.StyleItalic + } + if style.Bold { + aspect.Weight = metadata.WeightBold + } + + local := lookupLangFont(family, aspect) + if local != nil { + faces.addFace(local) + } + + return faces +} + // CachedFontFace returns a Font face held in memory. These are loaded from the current theme. -func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *FontCacheItem { - val, ok := fontCache.Load(style) +func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObject) *FontCacheItem { + if source != nil { + val, ok := fontCustomCache.Load(source) + if !ok { + face := loadMeasureFont(source) + if face == nil { + face = loadMeasureFont(theme.TextFont()) + } + faces := &dynamicFontMap{family: source.Name(), faces: []font.Face{face}} + + val = &FontCacheItem{Fonts: faces} + fontCustomCache.Store(source, val) + } + return val.(*FontCacheItem) + } + + scope := "" + if o != nil { // for overridden themes get the cache key right + scope = cache.WidgetScopeID(o) + } + + val, ok := fontCache.Load(cacheID{style: style, scope: scope}) if !ok { - var f1, f2 font.Face + var faces *dynamicFontMap + + th := theme.CurrentForWidget(o) + font1 := th.Font(style) + switch { case style.Monospace: - f1 = loadMeasureFont(theme.TextMonospaceFont()) - f2 = loadMeasureFont(theme.DefaultTextMonospaceFont()) + faces = lookupFaces(font1, theme.DefaultTextMonospaceFont(), fontscan.Monospace, style) case style.Bold: if style.Italic { - f1 = loadMeasureFont(theme.TextBoldItalicFont()) - f2 = loadMeasureFont(theme.DefaultTextBoldItalicFont()) + faces = lookupFaces(font1, theme.DefaultTextBoldItalicFont(), fontscan.SansSerif, style) } else { - f1 = loadMeasureFont(theme.TextBoldFont()) - f2 = loadMeasureFont(theme.DefaultTextBoldFont()) + faces = lookupFaces(font1, theme.DefaultTextBoldFont(), fontscan.SansSerif, style) } case style.Italic: - f1 = loadMeasureFont(theme.TextItalicFont()) - f2 = loadMeasureFont(theme.DefaultTextItalicFont()) + faces = lookupFaces(font1, theme.DefaultTextItalicFont(), fontscan.SansSerif, style) case style.Symbol: - f1 = loadMeasureFont(theme.SymbolFont()) - f2 = loadMeasureFont(theme.DefaultSymbolFont()) + th := theme.SymbolFont() + fallback := theme.DefaultSymbolFont() + f1 := loadMeasureFont(th) + + if th == fallback { + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1}} + } else { + f2 := loadMeasureFont(fallback) + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1, f2}} + } default: - f1 = loadMeasureFont(theme.TextFont()) - f2 = loadMeasureFont(theme.DefaultTextFont()) + faces = lookupFaces(font1, theme.DefaultTextFont(), fontscan.SansSerif, style) } - if f1 == nil { - f1 = f2 - } - faces := []font.Face{f1, f2} - if emoji := theme.DefaultEmojiFont(); emoji != nil { - faces = append(faces, loadMeasureFont(emoji)) + if emoji := theme.DefaultEmojiFont(); !style.Symbol && emoji != nil { + faces.addFace(loadMeasureFont(emoji)) // TODO only one emoji - maybe others too } val = &FontCacheItem{Fonts: faces} - fontCache.Store(style, val) + fontCache.Store(cacheID{style: style, scope: scope}, val) } return val.(*FontCacheItem) @@ -70,34 +161,27 @@ func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *Fon // ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces func ClearFontCache() { - fontCache = &sync.Map{} + fontCustomCache = &sync.Map{} } // DrawString draws a string into an image. -func DrawString(dst draw.Image, s string, color color.Color, f []font.Face, fontSize, scale float32, tabWidth int) { +func DrawString(dst draw.Image, s string, color color.Color, f shaping.Fontmap, fontSize, scale float32, style fyne.TextStyle) { r := render.Renderer{ FontSize: fontSize, PixScale: scale, Color: color, } - // TODO avoid shaping twice! - sh := &shaping.HarfbuzzShaper{} - out := sh.Shape(shaping.Input{ - Text: []rune(s), - RunStart: 0, - RunEnd: len(s), - Face: f[0], - Size: fixed.I(int(fontSize * r.PixScale)), - }) - advance := float32(0) - y := int(math.Ceil(float64(fixed266ToFloat32(out.LineBounds.Ascent)))) - walkString(f, s, float32ToFixed266(fontSize), tabWidth, &advance, scale, func(run shaping.Output, x float32) { + y := math.MinInt + walkString(f, s, float32ToFixed266(fontSize), style, &advance, scale, func(run shaping.Output, x float32) { + if y == math.MinInt { + y = int(math.Ceil(float64(fixed266ToFloat32(run.LineBounds.Ascent) * r.PixScale))) + } if len(run.Glyphs) == 1 { if run.Glyphs[0].GlyphID == 0 { - r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f[0]) + r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f.ResolveFace(0xfffd)) return } } @@ -118,20 +202,20 @@ func loadMeasureFont(data fyne.Resource) font.Face { // MeasureString returns how far dot would advance by drawing s with f. // Tabs are translated into a dot location change. -func MeasureString(f []font.Face, s string, textSize float32, tabWidth int) (size fyne.Size, advance float32) { - return walkString(f, s, float32ToFixed266(textSize), tabWidth, &advance, 1, func(shaping.Output, float32) {}) +func MeasureString(f shaping.Fontmap, s string, textSize float32, style fyne.TextStyle) (size fyne.Size, advance float32) { + return walkString(f, s, float32ToFixed266(textSize), style, &advance, 1, func(shaping.Output, float32) {}) } // RenderedTextSize looks up how big a string would be if drawn on screen. // It also returns the distance from top to the text baseline. -func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) { - size, base := cache.GetFontMetrics(text, fontSize, style) +func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + size, base := cache.GetFontMetrics(text, fontSize, style, source) if base != 0 { return size, base } - size, base = measureText(text, fontSize, style) - cache.SetFontMetrics(text, fontSize, style, size, base) + size, base = measureText(text, fontSize, style, source) + cache.SetFontMetrics(text, fontSize, style, source, size, base) return size, base } @@ -143,9 +227,9 @@ func float32ToFixed266(f float32) fixed.Int26_6 { return fixed.Int26_6(float64(f) * (1 << 6)) } -func measureText(text string, fontSize float32, style fyne.TextStyle) (fyne.Size, float32) { - face := CachedFontFace(style, fontSize, 1) - return MeasureString(face.Fonts, text, fontSize, style.TabWidth) +func measureText(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) { + face := CachedFontFace(style, source, nil) + return MeasureString(face.Fonts, text, fontSize, style) } func tabStop(spacew, x float32, tabWidth int) float32 { @@ -158,7 +242,7 @@ func tabStop(spacew, x float32, tabWidth int) float32 { return tabw * float32(tabs) } -func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth int, advance *float32, scale float32, +func walkString(faces shaping.Fontmap, s string, textSize fixed.Int26_6, style fyne.TextStyle, advance *float32, scale float32, cb func(run shaping.Output, x float32)) (size fyne.Size, base float32) { s = strings.ReplaceAll(s, "\r", "") @@ -168,7 +252,7 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in RunStart: 0, RunEnd: 1, Direction: di.DirectionLTR, - Face: faces[0], + Face: faces.ResolveFace(' '), Size: textSize, } shaper := &shaping.HarfbuzzShaper{} @@ -180,7 +264,10 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in x := float32(0) spacew := scale * fontTabSpaceSize - ins := shaping.SplitByFontGlyphs(in, faces) + if style.Monospace { + spacew = scale * fixed266ToFloat32(out.Advance) + } + ins := shaping.SplitByFace(in, faces) for _, in := range ins { inEnd := in.RunEnd @@ -189,10 +276,9 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in if r == '\t' { if pending { in.RunEnd = i - out = shaper.Shape(in) - x = shapeCallback(shaper, in, out, x, scale, cb) + x = shapeCallback(shaper, in, x, scale, cb) } - x = tabStop(spacew, x, tabWidth) + x = tabStop(spacew, x, style.TabWidth) in.RunStart = i + 1 in.RunEnd = inEnd @@ -202,7 +288,7 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in } } - x = shapeCallback(shaper, in, out, x, scale, cb) + x = shapeCallback(shaper, in, x, scale, cb) } *advance = x @@ -210,8 +296,8 @@ func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth in fixed266ToFloat32(out.LineBounds.Ascent) } -func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output, x, scale float32, cb func(shaping.Output, float32)) float32 { - out = shaper.Shape(in) +func shapeCallback(shaper shaping.Shaper, in shaping.Input, x, scale float32, cb func(shaping.Output, float32)) float32 { + out := shaper.Shape(in) glyphs := out.Glyphs start := 0 pending := false @@ -248,7 +334,43 @@ func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output, } type FontCacheItem struct { - Fonts []font.Face + Fonts shaping.Fontmap +} + +type cacheID struct { + style fyne.TextStyle + scope string +} + +var fontCache = &sync.Map{} // map[cacheID]*FontCacheItem +var fontCustomCache = &sync.Map{} // map[string]*FontCacheItem for custom resources + +type noopLogger struct{} + +func (n noopLogger) Printf(string, ...interface{}) {} + +type dynamicFontMap struct { + faces []font.Face + family string } -var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem +func (d *dynamicFontMap) ResolveFace(r rune) font.Face { + + for _, f := range d.faces { + if _, ok := f.NominalGlyph(r); ok { + return f + } + } + + toAdd := lookupRuneFont(r, d.family, metadata.Aspect{}) + if toAdd != nil { + d.addFace(toAdd) + return toAdd + } + + return d.faces[0] +} + +func (d *dynamicFontMap) addFace(f font.Face) { + d.faces = append(d.faces, f) +} diff --git a/internal/painter/font_internal_test.go b/internal/painter/font_internal_test.go index fe5ba89620..2692434c86 100644 --- a/internal/painter/font_internal_test.go +++ b/internal/painter/font_internal_test.go @@ -1,5 +1,9 @@ +//go:build test + package painter +import "github.com/go-text/typesetting/fontscan" + // //func Test_compositeFace_Close(t *testing.T) { // chosenFont := &truetype.Font{} @@ -334,3 +338,7 @@ package painter // f.IndexInvoked = true // return f.IndexFunc(r) //} + +func loadSystemFonts(fm *fontscan.FontMap) error { + return nil +} diff --git a/internal/painter/font_prod.go b/internal/painter/font_prod.go new file mode 100644 index 0000000000..fb57e3122c --- /dev/null +++ b/internal/painter/font_prod.go @@ -0,0 +1,21 @@ +//go:build !test + +package painter + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/go-text/typesetting/fontscan" +) + +func loadSystemFonts(fm *fontscan.FontMap) error { + cacheDir := "" + if runtime.GOOS == "android" { + parent := os.Getenv("FILESDIR") + cacheDir = filepath.Join(parent, "fontcache") + } + + return fm.UseSystemFonts(cacheDir) +} diff --git a/internal/painter/font_test.go b/internal/painter/font_test.go index 6d6276dccf..8be4e69c5c 100644 --- a/internal/painter/font_test.go +++ b/internal/painter/font_test.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal/painter" + intTest "fyne.io/fyne/v2/internal/test" "fyne.io/fyne/v2/test" ) @@ -26,10 +27,10 @@ func TestCachedFontFace(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - got := painter.CachedFontFace(tt.style, 14, 1) + got := painter.CachedFontFace(tt.style, nil, nil) for _, r := range tt.runes { - _, ok := got.Fonts[0].NominalGlyph(r) - assert.True(t, ok, "symbol Font should include: %c", r) + f := got.Fonts.ResolveFace(r) + assert.NotNil(t, f, "symbol Font should include: %c", r) } }) } @@ -76,8 +77,10 @@ func TestDrawString(t *testing.T) { } { t.Run(name, func(t *testing.T) { img := image.NewNRGBA(image.Rect(0, 0, 300, 100)) - f := painter.CachedFontFace(tt.style, tt.size, 1) - painter.DrawString(img, tt.string, tt.color, f.Fonts, tt.size, 1, tt.tabWidth) + f := painter.CachedFontFace(tt.style, nil, nil) + + fontMap := &intTest.FontMap{f.Fonts.ResolveFace(' ')} // first (ascii) font + painter.DrawString(img, tt.string, tt.color, fontMap, tt.size, 1, fyne.TextStyle{TabWidth: tt.tabWidth}) test.AssertImageMatches(t, "font/"+tt.want, img) }) } @@ -114,16 +117,17 @@ func TestMeasureString(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - face := painter.CachedFontFace(tt.style, tt.size, 1) - got, _ := painter.MeasureString(face.Fonts, tt.string, tt.size, tt.tabWidth) + faces := painter.CachedFontFace(tt.style, nil, nil) + fontMap := &intTest.FontMap{faces.Fonts.ResolveFace(' ')} // first (ascii) font + got, _ := painter.MeasureString(fontMap, tt.string, tt.size, fyne.TextStyle{TabWidth: tt.tabWidth}) assert.Equal(t, tt.want, got.Width) }) } } func TestRenderedTextSize(t *testing.T) { - size1, baseline1 := painter.RenderedTextSize("Hello World!", 20, fyne.TextStyle{}) - size2, baseline2 := painter.RenderedTextSize("\rH\re\rl\rl\ro\r \rW\ro\rr\rl\rd\r!\r", 20, fyne.TextStyle{}) + size1, baseline1 := painter.RenderedTextSize("Hello World!", 20, fyne.TextStyle{}, nil) + size2, baseline2 := painter.RenderedTextSize("\rH\re\rl\rl\ro\r \rW\ro\rr\rl\rd\r!\r", 20, fyne.TextStyle{}, nil) assert.Equal(t, int(size1.Width), int(size2.Width)) assert.Equal(t, size1.Height, size2.Height) assert.Equal(t, baseline1, baseline2) diff --git a/internal/painter/gl/texture.go b/internal/painter/gl/texture.go index 1cdaaea298..5989dcd2aa 100644 --- a/internal/painter/gl/texture.go +++ b/internal/painter/gl/texture.go @@ -1,6 +1,7 @@ package gl import ( + "errors" "fmt" "image" "image/draw" @@ -37,7 +38,7 @@ func (p *painter) getTexture(object fyne.CanvasObject, creator func(canvasObject cache.SetTexture(object, texture, p.canvas) } if !cache.IsValid(texture) { - return noTexture, fmt.Errorf("no texture available") + return noTexture, errors.New("no texture available") } return Texture(texture), nil } @@ -152,8 +153,8 @@ func (p *painter) newGlTextTexture(obj fyne.CanvasObject) Texture { height := int(math.Ceil(float64(p.textureScale(bounds.Height)))) img := image.NewNRGBA(image.Rect(0, 0, width, height)) - face := paint.CachedFontFace(text.TextStyle, text.TextSize*p.canvas.Scale(), p.texScale) - paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle.TabWidth) + face := paint.CachedFontFace(text.TextStyle, text.FontSource, text) + paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle) return p.imgToTexture(img, canvas.ImageScaleSmooth) } diff --git a/internal/painter/software/draw.go b/internal/painter/software/draw.go index 23043e6d38..9b246177b3 100644 --- a/internal/painter/software/draw.go +++ b/internal/painter/software/draw.go @@ -142,8 +142,8 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N color = theme.ForegroundColor() } - face := painter.CachedFontFace(text.TextStyle, text.TextSize*c.Scale(), 1) - painter.DrawString(txtImg, text.Text, color, face.Fonts, text.TextSize, c.Scale(), text.TextStyle.TabWidth) + face := painter.CachedFontFace(text.TextStyle, text.FontSource, text) + painter.DrawString(txtImg, text.Text, color, face.Fonts, text.TextSize, c.Scale(), text.TextStyle) size := text.Size() offsetX := float32(0) diff --git a/internal/painter/testdata/font/hello_TAB_world_bold_italic_size_27.42_height_42_tab_width_3.png b/internal/painter/testdata/font/hello_TAB_world_bold_italic_size_27.42_height_42_tab_width_3.png index 1c1a6e3f4e..599095f408 100644 Binary files a/internal/painter/testdata/font/hello_TAB_world_bold_italic_size_27.42_height_42_tab_width_3.png and b/internal/painter/testdata/font/hello_TAB_world_bold_italic_size_27.42_height_42_tab_width_3.png differ diff --git a/internal/repository/file_test.go b/internal/repository/file_test.go index b9fbedea2a..67223f3037 100644 --- a/internal/repository/file_test.go +++ b/internal/repository/file_test.go @@ -41,16 +41,12 @@ func TestFileRepositoryRegistration(t *testing.T) { } func TestFileRepositoryExists(t *testing.T) { - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() existsPath := path.Join(dir, "exists") notExistsPath := path.Join(dir, "notExists") - err = os.WriteFile(existsPath, []byte{1, 2, 3, 4}, 0755) + err := os.WriteFile(existsPath, []byte{1, 2, 3, 4}, 0755) if err != nil { t.Fatal(err) } @@ -65,18 +61,13 @@ func TestFileRepositoryExists(t *testing.T) { } func TestFileRepositoryReader(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to test with. fooPath := path.Join(dir, "foo") barPath := path.Join(dir, "bar") bazPath := path.Join(dir, "baz") - err = os.WriteFile(fooPath, []byte{}, 0755) + err := os.WriteFile(fooPath, []byte{}, 0755) if err != nil { t.Fatal(err) } @@ -100,6 +91,7 @@ func TestFileRepositoryReader(t *testing.T) { fooData, err := io.ReadAll(fooReader) assert.Equal(t, []byte{}, fooData) assert.Nil(t, err) + fooReader.Close() // Make sure we can read the file with data. barReader, err := storage.Reader(bar) @@ -107,10 +99,12 @@ func TestFileRepositoryReader(t *testing.T) { barData, err := io.ReadAll(barReader) assert.Equal(t, []byte{1, 2, 3}, barData) assert.Nil(t, err) + barReader.Close() // Make sure we get an error if the file doesn't exist. - _, err = storage.Reader(baz) + bazReader, err := storage.Reader(baz) assert.NotNil(t, err) + bazReader.Close() // Also test that CanRead returns the expected results. fooCanRead, err := storage.CanRead(foo) @@ -127,19 +121,14 @@ func TestFileRepositoryReader(t *testing.T) { } func TestFileRepositoryWriter(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to test with. fooPath := path.Join(dir, "foo") barPath := path.Join(dir, "bar") bazPath := path.Join(dir, "baz") spamHamPath := path.Join(dir, "spam", "ham") - err = os.WriteFile(fooPath, []byte{}, 0755) + err := os.WriteFile(fooPath, []byte{}, 0755) if err != nil { t.Fatal(err) } @@ -247,18 +236,13 @@ func TestFileRepositoryWriter(t *testing.T) { } func TestFileRepositoryCanWrite(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to test with. fooPath := path.Join(dir, "foo") barPath := path.Join(dir, "bar") bazPath := path.Join(dir, "baz") - err = os.WriteFile(fooPath, []byte{}, 0755) + err := os.WriteFile(fooPath, []byte{}, 0755) if err != nil { t.Fatal(err) } @@ -365,17 +349,12 @@ func TestFileRepositoryChild(t *testing.T) { } func TestFileRepositoryCopy(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to test with. fooPath := path.Join(dir, "foo") barPath := path.Join(dir, "bar") - err = os.WriteFile(fooPath, []byte{1, 2, 3, 4, 5}, 0755) + err := os.WriteFile(fooPath, []byte{1, 2, 3, 4, 5}, 0755) if err != nil { t.Fatal(err) } @@ -396,17 +375,12 @@ func TestFileRepositoryCopy(t *testing.T) { } func TestFileRepositoryMove(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to test with. fooPath := path.Join(dir, "foo") barPath := path.Join(dir, "bar") - err = os.WriteFile(fooPath, []byte{1, 2, 3, 4, 5}, 0755) + err := os.WriteFile(fooPath, []byte{1, 2, 3, 4, 5}, 0755) if err != nil { t.Fatal(err) } @@ -429,12 +403,7 @@ func TestFileRepositoryMove(t *testing.T) { } func TestFileRepositoryListing(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() // Create some files to tests with. fooPath := path.Join(dir, "foo") @@ -466,12 +435,7 @@ func TestFileRepositoryListing(t *testing.T) { } func TestFileRepositoryCreateListable(t *testing.T) { - // Set up a temporary directory. - dir, err := os.MkdirTemp("", "FyneInternalRepositoryFileTest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() f := NewFileRepository() repository.Register("file", f) @@ -482,7 +446,7 @@ func TestFileRepositoryCreateListable(t *testing.T) { fooBar := storage.NewFileURI(fooBarPath) // Creating a dir with no parent should fail - err = storage.CreateListable(fooBar) + err := storage.CreateListable(fooBar) assert.NotNil(t, err) // Creating foo should work though diff --git a/internal/test/text.go b/internal/test/text.go new file mode 100644 index 0000000000..a4a230faf0 --- /dev/null +++ b/internal/test/text.go @@ -0,0 +1,18 @@ +package test + +import "github.com/go-text/typesetting/font" + +type FontMap []font.Face + +func (f FontMap) ResolveFace(r rune) font.Face { + if len(f) == 1 { + return f[0] + } + + face := f.ResolveFace(r) + if face != nil { + return face + } + + return f[0] +} diff --git a/internal/test/util_test.go b/internal/test/util_test.go index 1f825953d8..14e7dd20a6 100644 --- a/internal/test/util_test.go +++ b/internal/test/util_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/internal/test" "fyne.io/fyne/v2/theme" @@ -27,7 +28,7 @@ func TestAssertImageMatches(t *testing.T) { face, err := font.ParseTTF(bytes.NewReader(theme.TextFont().Content())) assert.Nil(t, err) - painter.DrawString(txtImg, "Hello!", color.Black, []font.Face{face}, 25, 1, 4) + painter.DrawString(txtImg, "Hello!", color.Black, &test.FontMap{face}, 25, 1, fyne.TextStyle{TabWidth: 4}) draw.Draw(img, bounds, txtImg, image.Point{}, draw.Over) tt := &testing.T{} diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 4dc8488091..fafd478697 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -461,7 +461,9 @@ func (s *Scroll) refreshWithoutOffsetUpdate() { // Scrolled is called when an input device triggers a scroll event func (s *Scroll) Scrolled(ev *fyne.ScrollEvent) { - s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY) + if s.Direction != ScrollNone { + s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY) + } } func (s *Scroll) scrollBy(dx, dy float32) { diff --git a/internal/widget/scroller_test.go b/internal/widget/scroller_test.go index a8a5e81963..380664355a 100644 --- a/internal/widget/scroller_test.go +++ b/internal/widget/scroller_test.go @@ -223,6 +223,19 @@ func TestScrollContainer_Scrolled_Back(t *testing.T) { assert.Equal(t, float32(0), scroll.Offset.Y) } +func TestScrollContainer_Scrolled_ScrollNone(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Direction = ScrollNone + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) +} + func TestScrollContainer_Scrolled_BackLimit(t *testing.T) { rect := canvas.NewRectangle(color.Black) scroll := NewScroll(rect) diff --git a/internal/widget/testdata/shadow/theme_ugly.png b/internal/widget/testdata/shadow/theme_ugly.png index 3d282a46ac..3f9125ac85 100644 Binary files a/internal/widget/testdata/shadow/theme_ugly.png and b/internal/widget/testdata/shadow/theme_ugly.png differ diff --git a/lang/locale.go b/lang/locale.go index 4dba05a568..77f396fe72 100644 --- a/lang/locale.go +++ b/lang/locale.go @@ -1,9 +1,10 @@ package lang import ( - "fyne.io/fyne/v2" "github.com/jeandeaual/go-locale" "golang.org/x/text/language" + + "fyne.io/fyne/v2" ) // SystemLocale returns the primary locale on the current system. @@ -12,6 +13,8 @@ func SystemLocale() fyne.Locale { loc, err := locale.GetLocale() if err != nil { fyne.LogError("Failed to look up user locale", err) + } + if loc == "" { loc = "en" } diff --git a/test/testapp.go b/test/app.go similarity index 78% rename from test/testapp.go rename to test/app.go index bd89e0dd8b..d1b5566f49 100644 --- a/test/testapp.go +++ b/test/app.go @@ -8,7 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal" - "fyne.io/fyne/v2/internal/app" + intapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/internal/test" @@ -20,13 +20,13 @@ func init() { NewApp() } -type testApp struct { - driver *testDriver +type app struct { + driver *driver settings *testSettings prefs fyne.Preferences propertyLock sync.RWMutex storage fyne.Storage - lifecycle fyne.Lifecycle + lifecycle intapp.Lifecycle cloud fyne.CloudProvider // user action variables @@ -34,51 +34,51 @@ type testApp struct { lastNotification *fyne.Notification } -func (a *testApp) CloudProvider() fyne.CloudProvider { +func (a *app) CloudProvider() fyne.CloudProvider { return a.cloud } -func (a *testApp) Icon() fyne.Resource { +func (a *app) Icon() fyne.Resource { return nil } -func (a *testApp) SetIcon(fyne.Resource) { +func (a *app) SetIcon(fyne.Resource) { // no-op } -func (a *testApp) NewWindow(title string) fyne.Window { +func (a *app) NewWindow(title string) fyne.Window { return a.driver.CreateWindow(title) } -func (a *testApp) OpenURL(url *url.URL) error { +func (a *app) OpenURL(url *url.URL) error { // no-op return nil } -func (a *testApp) Run() { +func (a *app) Run() { // no-op } -func (a *testApp) Quit() { +func (a *app) Quit() { // no-op } -func (a *testApp) UniqueID() string { +func (a *app) UniqueID() string { return "testApp" // TODO should this be randomised? } -func (a *testApp) Driver() fyne.Driver { +func (a *app) Driver() fyne.Driver { return a.driver } -func (a *testApp) SendNotification(notify *fyne.Notification) { +func (a *app) SendNotification(notify *fyne.Notification) { a.propertyLock.Lock() defer a.propertyLock.Unlock() a.lastNotification = notify } -func (a *testApp) SetCloudProvider(p fyne.CloudProvider) { +func (a *app) SetCloudProvider(p fyne.CloudProvider) { if p == nil { a.cloud = nil return @@ -87,34 +87,34 @@ func (a *testApp) SetCloudProvider(p fyne.CloudProvider) { a.transitionCloud(p) } -func (a *testApp) Settings() fyne.Settings { +func (a *app) Settings() fyne.Settings { return a.settings } -func (a *testApp) Preferences() fyne.Preferences { +func (a *app) Preferences() fyne.Preferences { return a.prefs } -func (a *testApp) Storage() fyne.Storage { +func (a *app) Storage() fyne.Storage { return a.storage } -func (a *testApp) Lifecycle() fyne.Lifecycle { - return a.lifecycle +func (a *app) Lifecycle() fyne.Lifecycle { + return &a.lifecycle } -func (a *testApp) Metadata() fyne.AppMetadata { +func (a *app) Metadata() fyne.AppMetadata { return fyne.AppMetadata{} // just dummy data } -func (a *testApp) lastAppliedTheme() fyne.Theme { +func (a *app) lastAppliedTheme() fyne.Theme { a.propertyLock.Lock() defer a.propertyLock.Unlock() return a.appliedTheme } -func (a *testApp) transitionCloud(p fyne.CloudProvider) { +func (a *app) transitionCloud(p fyne.CloudProvider) { if a.cloud != nil { a.cloud.Cleanup(a) } @@ -162,8 +162,7 @@ func NewApp() fyne.App { settings := &testSettings{scale: 1.0, theme: Theme()} prefs := internal.NewInMemoryPreferences() store := &testStorage{} - test := &testApp{settings: settings, prefs: prefs, storage: store, driver: NewDriver().(*testDriver), - lifecycle: &app.Lifecycle{}} + test := &app{settings: settings, prefs: prefs, storage: store, driver: NewDriver().(*driver)} root, _ := store.docRootURI() store.Docs = &internal.Docs{RootDocURI: root} painter.ClearFontCache() @@ -178,7 +177,7 @@ func NewApp() fyne.App { test.propertyLock.Lock() painter.ClearFontCache() cache.ResetThemeCaches() - app.ApplySettings(test.Settings(), test) + intapp.ApplySettings(test.Settings(), test) test.appliedTheme = test.Settings().Theme() test.propertyLock.Unlock() diff --git a/test/testapp_test.go b/test/app_test.go similarity index 100% rename from test/testapp_test.go rename to test/app_test.go diff --git a/test/testcanvas.go b/test/canvas.go similarity index 67% rename from test/testcanvas.go rename to test/canvas.go index 275ed91171..d4c7c07458 100644 --- a/test/testcanvas.go +++ b/test/canvas.go @@ -8,15 +8,13 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/internal" - "fyne.io/fyne/v2/internal/app" + intapp "fyne.io/fyne/v2/internal/app" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/scale" "fyne.io/fyne/v2/theme" ) -var ( - dummyCanvas fyne.Canvas -) +var dummyCanvas WindowlessCanvas // WindowlessCanvas provides functionality for a canvas to operate without a window type WindowlessCanvas interface { @@ -28,13 +26,13 @@ type WindowlessCanvas interface { SetScale(float32) } -type testCanvas struct { +type canvas struct { size fyne.Size scale float32 content fyne.CanvasObject overlays *internal.OverlayStack - focusMgr *app.FocusManager + focusMgr *intapp.FocusManager hovered desktop.Hoverable padded bool transparent bool @@ -59,11 +57,11 @@ func Canvas() fyne.Canvas { // NewCanvas returns a single use in-memory canvas used for testing. // This canvas has no painter so calls to Capture() will return a blank image. func NewCanvas() WindowlessCanvas { - c := &testCanvas{ - focusMgr: app.NewFocusManager(nil), + c := &canvas{ + focusMgr: intapp.NewFocusManager(nil), padded: true, scale: 1.0, - size: fyne.NewSize(10, 10), + size: fyne.NewSize(100, 100), } c.overlays = &internal.OverlayStack{Canvas: c} return c @@ -72,10 +70,10 @@ func NewCanvas() WindowlessCanvas { // NewCanvasWithPainter allows creation of an in-memory canvas with a specific painter. // The painter will be used to render in the Capture() call. func NewCanvasWithPainter(painter SoftwarePainter) WindowlessCanvas { - canvas := NewCanvas().(*testCanvas) - canvas.painter = painter + c := NewCanvas().(*canvas) + c.painter = painter - return canvas + return c } // NewTransparentCanvasWithPainter allows creation of an in-memory canvas with a specific painter without a background color. @@ -83,15 +81,16 @@ func NewCanvasWithPainter(painter SoftwarePainter) WindowlessCanvas { // // Since: 2.2 func NewTransparentCanvasWithPainter(painter SoftwarePainter) WindowlessCanvas { - canvas := NewCanvasWithPainter(painter).(*testCanvas) - canvas.transparent = true + c := NewCanvasWithPainter(painter).(*canvas) + c.transparent = true - return canvas + return c } -func (c *testCanvas) Capture() image.Image { +func (c *canvas) Capture() image.Image { cache.Clean(true) - bounds := image.Rect(0, 0, scale.ToScreenCoordinate(c, c.Size().Width), scale.ToScreenCoordinate(c, c.Size().Height)) + size := c.Size() + bounds := image.Rect(0, 0, scale.ToScreenCoordinate(c, size.Width), scale.ToScreenCoordinate(c, size.Height)) img := image.NewNRGBA(bounds) if !c.transparent { draw.Draw(img, bounds, image.NewUniform(theme.BackgroundColor()), image.Point{}, draw.Src) @@ -104,69 +103,69 @@ func (c *testCanvas) Capture() image.Image { return img } -func (c *testCanvas) Content() fyne.CanvasObject { +func (c *canvas) Content() fyne.CanvasObject { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.content } -func (c *testCanvas) Focus(obj fyne.Focusable) { +func (c *canvas) Focus(obj fyne.Focusable) { c.focusManager().Focus(obj) } -func (c *testCanvas) FocusNext() { +func (c *canvas) FocusNext() { c.focusManager().FocusNext() } -func (c *testCanvas) FocusPrevious() { +func (c *canvas) FocusPrevious() { c.focusManager().FocusPrevious() } -func (c *testCanvas) Focused() fyne.Focusable { +func (c *canvas) Focused() fyne.Focusable { return c.focusManager().Focused() } -func (c *testCanvas) InteractiveArea() (fyne.Position, fyne.Size) { - return fyne.Position{}, c.Size() +func (c *canvas) InteractiveArea() (fyne.Position, fyne.Size) { + return fyne.NewPos(2, 3), c.Size().SubtractWidthHeight(4, 5) } -func (c *testCanvas) OnTypedKey() func(*fyne.KeyEvent) { +func (c *canvas) OnTypedKey() func(*fyne.KeyEvent) { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.onTypedKey } -func (c *testCanvas) OnTypedRune() func(rune) { +func (c *canvas) OnTypedRune() func(rune) { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.onTypedRune } -func (c *testCanvas) Overlays() fyne.OverlayStack { +func (c *canvas) Overlays() fyne.OverlayStack { c.propertyLock.Lock() defer c.propertyLock.Unlock() return c.overlays } -func (c *testCanvas) Padded() bool { +func (c *canvas) Padded() bool { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.padded } -func (c *testCanvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { - return int(float32(pos.X) * c.scale), int(float32(pos.Y) * c.scale) +func (c *canvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { + return int(pos.X * c.scale), int(pos.Y * c.scale) } -func (c *testCanvas) Refresh(fyne.CanvasObject) { +func (c *canvas) Refresh(fyne.CanvasObject) { } -func (c *testCanvas) Resize(size fyne.Size) { +func (c *canvas) Resize(size fyne.Size) { c.propertyLock.Lock() content := c.content overlays := c.overlays @@ -194,53 +193,54 @@ func (c *testCanvas) Resize(size fyne.Size) { } if padded { - content.Resize(size.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2))) - content.Move(fyne.NewPos(theme.Padding(), theme.Padding())) + padding := theme.Padding() + content.Resize(size.Subtract(fyne.NewSquareSize(padding * 2))) + content.Move(fyne.NewSquareOffsetPos(padding)) } else { content.Resize(size) content.Move(fyne.NewPos(0, 0)) } } -func (c *testCanvas) Scale() float32 { +func (c *canvas) Scale() float32 { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.scale } -func (c *testCanvas) SetContent(content fyne.CanvasObject) { +func (c *canvas) SetContent(content fyne.CanvasObject) { c.propertyLock.Lock() c.content = content - c.focusMgr = app.NewFocusManager(c.content) + c.focusMgr = intapp.NewFocusManager(c.content) c.propertyLock.Unlock() if content == nil { return } - padding := fyne.NewSize(0, 0) + minSize := content.MinSize() if c.padded { - padding = fyne.NewSize(theme.Padding()*2, theme.Padding()*2) + minSize = minSize.Add(fyne.NewSquareSize(theme.Padding() * 2)) } - c.Resize(content.MinSize().Add(padding)) + c.Resize(minSize) } -func (c *testCanvas) SetOnTypedKey(handler func(*fyne.KeyEvent)) { +func (c *canvas) SetOnTypedKey(handler func(*fyne.KeyEvent)) { c.propertyLock.Lock() defer c.propertyLock.Unlock() c.onTypedKey = handler } -func (c *testCanvas) SetOnTypedRune(handler func(rune)) { +func (c *canvas) SetOnTypedRune(handler func(rune)) { c.propertyLock.Lock() defer c.propertyLock.Unlock() c.onTypedRune = handler } -func (c *testCanvas) SetPadded(padded bool) { +func (c *canvas) SetPadded(padded bool) { c.propertyLock.Lock() c.padded = padded c.propertyLock.Unlock() @@ -248,25 +248,25 @@ func (c *testCanvas) SetPadded(padded bool) { c.Resize(c.Size()) } -func (c *testCanvas) SetScale(scale float32) { +func (c *canvas) SetScale(scale float32) { c.propertyLock.Lock() defer c.propertyLock.Unlock() c.scale = scale } -func (c *testCanvas) Size() fyne.Size { +func (c *canvas) Size() fyne.Size { c.propertyLock.RLock() defer c.propertyLock.RUnlock() return c.size } -func (c *testCanvas) Unfocus() { +func (c *canvas) Unfocus() { c.focusManager().Focus(nil) } -func (c *testCanvas) focusManager() *app.FocusManager { +func (c *canvas) focusManager() *intapp.FocusManager { c.propertyLock.RLock() defer c.propertyLock.RUnlock() if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil { @@ -275,12 +275,13 @@ func (c *testCanvas) focusManager() *app.FocusManager { return c.focusMgr } -func (c *testCanvas) objectTrees() []fyne.CanvasObject { - trees := make([]fyne.CanvasObject, 0, len(c.Overlays().List())+1) +func (c *canvas) objectTrees() []fyne.CanvasObject { + overlays := c.Overlays().List() + trees := make([]fyne.CanvasObject, 0, len(overlays)+1) if c.content != nil { trees = append(trees, c.content) } - trees = append(trees, c.Overlays().List()...) + trees = append(trees, overlays...) return trees } diff --git a/test/testcanvas_test.go b/test/canvas_test.go similarity index 69% rename from test/testcanvas_test.go rename to test/canvas_test.go index 9796b210e5..c232c609e5 100644 --- a/test/testcanvas_test.go +++ b/test/canvas_test.go @@ -10,7 +10,7 @@ import ( "fyne.io/fyne/v2/theme" ) -func TestTestCanvas_Capture(t *testing.T) { +func Test_canvas_Capture(t *testing.T) { c := NewCanvas() c.Size() @@ -26,7 +26,25 @@ func TestTestCanvas_Capture(t *testing.T) { assert.Equal(t, a1, a2) } -func TestTestCanvas_TransparentCapture(t *testing.T) { +func Test_canvas_InteractiveArea(t *testing.T) { + c := NewCanvas() + c.Resize(fyne.NewSize(600, 400)) + pos, size := c.InteractiveArea() + assert.Equal(t, fyne.NewPos(2, 3), pos) + assert.Equal(t, fyne.NewSize(596, 395), size) +} + +func Test_canvas_PixelCoordinateAtPosition(t *testing.T) { + c := NewCanvas().(*canvas) + + pos := fyne.NewPos(4, 4) + c.scale = 2.5 + x, y := c.PixelCoordinateForPosition(pos) + assert.Equal(t, 10, x) + assert.Equal(t, 10, y) +} + +func Test_canvas_TransparentCapture(t *testing.T) { c := NewTransparentCanvasWithPainter(nil) c.Size() @@ -41,13 +59,3 @@ func TestTestCanvas_TransparentCapture(t *testing.T) { assert.Equal(t, b1, b2) assert.Equal(t, a1, a2) } - -func TestGlCanvas_PixelCoordinateAtPosition(t *testing.T) { - c := NewCanvas().(*testCanvas) - - pos := fyne.NewPos(4, 4) - c.scale = 2.5 - x, y := c.PixelCoordinateForPosition(pos) - assert.Equal(t, 10, x) - assert.Equal(t, 10, y) -} diff --git a/test/testclipboard.go b/test/clipboard.go similarity index 59% rename from test/testclipboard.go rename to test/clipboard.go index d98f689aa0..db37f3c154 100644 --- a/test/testclipboard.go +++ b/test/clipboard.go @@ -2,19 +2,19 @@ package test import "fyne.io/fyne/v2" -type testClipboard struct { +type clipboard struct { content string } -func (c *testClipboard) Content() string { +func (c *clipboard) Content() string { return c.content } -func (c *testClipboard) SetContent(content string) { +func (c *clipboard) SetContent(content string) { c.content = content } // NewClipboard returns a single use in-memory clipboard used for testing func NewClipboard() fyne.Clipboard { - return &testClipboard{} + return &clipboard{} } diff --git a/test/testdriver.go b/test/driver.go similarity index 50% rename from test/testdriver.go rename to test/driver.go index 12d34e5d74..8d26384b84 100644 --- a/test/testdriver.go +++ b/test/driver.go @@ -6,7 +6,7 @@ import ( "time" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/internal/driver" + intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/internal/painter/software" intRepo "fyne.io/fyne/v2/internal/repository" @@ -18,21 +18,25 @@ type SoftwarePainter interface { Paint(fyne.Canvas) image.Image } -type testDriver struct { - device *device +type driver struct { + device device painter SoftwarePainter windows []fyne.Window windowsMutex sync.RWMutex } // Declare conformity with Driver -var _ fyne.Driver = (*testDriver)(nil) +var _ fyne.Driver = (*driver)(nil) // NewDriver sets up and registers a new dummy driver for test purpose func NewDriver() fyne.Driver { - drv := &testDriver{windowsMutex: sync.RWMutex{}} + drv := &driver{windowsMutex: sync.RWMutex{}} repository.Register("file", intRepo.NewFileRepository()) + httpHandler := intRepo.NewHTTPRepository() + repository.Register("http", httpHandler) + repository.Register("https", httpHandler) + // make a single dummy window for rendering tests drv.CreateWindow("") @@ -42,99 +46,97 @@ func NewDriver() fyne.Driver { // NewDriverWithPainter creates a new dummy driver that will pass the given // painter to all canvases created func NewDriverWithPainter(painter SoftwarePainter) fyne.Driver { - return &testDriver{ - painter: painter, - windowsMutex: sync.RWMutex{}, - } + return &driver{painter: painter} } -func (d *testDriver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { +func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { c := d.CanvasForObject(co) if c == nil { return fyne.NewPos(0, 0) } - tc := c.(*testCanvas) - return driver.AbsolutePositionForObject(co, tc.objectTrees()) + tc := c.(*canvas) + pos := intdriver.AbsolutePositionForObject(co, tc.objectTrees()) + inset, _ := c.InteractiveArea() + return pos.Subtract(inset) } -func (d *testDriver) AllWindows() []fyne.Window { +func (d *driver) AllWindows() []fyne.Window { d.windowsMutex.RLock() defer d.windowsMutex.RUnlock() return d.windows } -func (d *testDriver) CanvasForObject(fyne.CanvasObject) fyne.Canvas { +func (d *driver) CanvasForObject(fyne.CanvasObject) fyne.Canvas { d.windowsMutex.RLock() defer d.windowsMutex.RUnlock() // cheating: probably the last created window is meant return d.windows[len(d.windows)-1].Canvas() } -func (d *testDriver) CreateWindow(string) fyne.Window { - canvas := NewCanvas().(*testCanvas) +func (d *driver) CreateWindow(string) fyne.Window { + c := NewCanvas().(*canvas) if d.painter != nil { - canvas.painter = d.painter + c.painter = d.painter } else { - canvas.painter = software.NewPainter() + c.painter = software.NewPainter() } - window := &testWindow{canvas: canvas, driver: d} - window.clipboard = &testClipboard{} + w := &window{canvas: c, driver: d} d.windowsMutex.Lock() - d.windows = append(d.windows, window) + d.windows = append(d.windows, w) d.windowsMutex.Unlock() - return window + return w } -func (d *testDriver) Device() fyne.Device { - if d.device == nil { - d.device = &device{} - } - return d.device +func (d *driver) Device() fyne.Device { + return &d.device } // RenderedTextSize looks up how bit a string would be if drawn on screen -func (d *testDriver) RenderedTextSize(text string, size float32, style fyne.TextStyle) (fyne.Size, float32) { - return painter.RenderedTextSize(text, size, style) +func (d *driver) RenderedTextSize(text string, size float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) { + return painter.RenderedTextSize(text, size, style, source) } -func (d *testDriver) Run() { +func (d *driver) Run() { // no-op } -func (d *testDriver) StartAnimation(a *fyne.Animation) { +func (d *driver) StartAnimation(a *fyne.Animation) { // currently no animations in test app, we just initialise it and leave a.Tick(1.0) } -func (d *testDriver) StopAnimation(a *fyne.Animation) { +func (d *driver) StopAnimation(a *fyne.Animation) { // currently no animations in test app, do nothing } -func (d *testDriver) Quit() { +func (d *driver) Quit() { // no-op } -func (d *testDriver) removeWindow(w *testWindow) { +func (d *driver) removeWindow(w *window) { d.windowsMutex.Lock() i := 0 - for _, window := range d.windows { - if window == w { + for _, win := range d.windows { + if win == w { break } i++ } - d.windows = append(d.windows[:i], d.windows[i+1:]...) + copy(d.windows[i:], d.windows[i+1:]) + d.windows[len(d.windows)-1] = nil // Allow the garbage collector to reclaim the memory. + d.windows = d.windows[:len(d.windows)-1] + d.windowsMutex.Unlock() } -func (d *testDriver) DoubleTapDelay() time.Duration { +func (d *driver) DoubleTapDelay() time.Duration { return 300 * time.Millisecond } -func (d *testDriver) SetDisableScreenBlanking(_ bool) { +func (d *driver) SetDisableScreenBlanking(_ bool) { // no-op for test } diff --git a/test/driver_test.go b/test/driver_test.go new file mode 100644 index 0000000000..923b35fa27 --- /dev/null +++ b/test/driver_test.go @@ -0,0 +1,29 @@ +package test + +import ( + "image/color" + "testing" + + "github.com/stretchr/testify/assert" + + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" +) + +func Test_driver_AbsolutePositionForObject(t *testing.T) { + d := &driver{} + w := d.CreateWindow("Test Window") + o := fynecanvas.NewRectangle(color.Black) + w.SetContent(o) + w.Resize(fyne.NewSize(320, 200)) + + t.Run("for padded window", func(t *testing.T) { + w.SetPadded(true) + assert.Equal(t, fyne.NewPos(2, 1), d.AbsolutePositionForObject(o), "safe area offset (2,3) is subtracted") + }) + + t.Run("for non-padded window", func(t *testing.T) { + w.SetPadded(false) + assert.Equal(t, fyne.NewPos(-2, -3), d.AbsolutePositionForObject(o), "safe area offset (2,3) is subtracted") + }) +} diff --git a/test/testfile.go b/test/file.go similarity index 74% rename from test/testfile.go rename to test/file.go index 9e986a7a6a..ee49a9e753 100644 --- a/test/testfile.go +++ b/test/file.go @@ -1,6 +1,7 @@ package test import ( + "errors" "fmt" "io" "os" @@ -10,6 +11,8 @@ import ( "fyne.io/fyne/v2/storage" ) +var errUnsupportedURLProtocol = errors.New("unsupported URL protocol") + type file struct { *os.File path string @@ -44,28 +47,30 @@ func (f *file) URI() fyne.URI { func openFile(uri fyne.URI, create bool) (*file, error) { if uri.Scheme() != "file" { - return nil, fmt.Errorf("unsupported URL protocol") + return nil, errUnsupportedURLProtocol } - path := uri.String()[7:] - f, err := os.Open(path) - if err != nil && create { - f, err = os.Create(path) + path := uri.Path() + if create { + f, err := os.Create(path) + return &file{File: f, path: path}, err } + + f, err := os.Open(path) return &file{File: f, path: path}, err } -func (d *testDriver) FileReaderForURI(uri fyne.URI) (fyne.URIReadCloser, error) { +func (d *driver) FileReaderForURI(uri fyne.URI) (fyne.URIReadCloser, error) { return openFile(uri, false) } -func (d *testDriver) FileWriterForURI(uri fyne.URI) (fyne.URIWriteCloser, error) { +func (d *driver) FileWriterForURI(uri fyne.URI) (fyne.URIWriteCloser, error) { return openFile(uri, true) } -func (d *testDriver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) { +func (d *driver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) { if uri.Scheme() != "file" { - return nil, fmt.Errorf("unsupported URL protocol") + return nil, errUnsupportedURLProtocol } path := uri.String()[len(uri.Scheme())+3 : len(uri.String())] @@ -83,7 +88,7 @@ func (d *testDriver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) { func (d *directory) List() ([]fyne.URI, error) { if d.Scheme() != "file" { - return nil, fmt.Errorf("unsupported URL protocol") + return nil, errUnsupportedURLProtocol } path := d.String()[len(d.Scheme())+3 : len(d.String())] diff --git a/test/markup_renderer.go b/test/markup_renderer.go index c6ed604862..eea4c684a4 100644 --- a/test/markup_renderer.go +++ b/test/markup_renderer.go @@ -9,10 +9,9 @@ import ( "unsafe" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/internal/cache" + fynecanvas "fyne.io/fyne/v2/canvas" col "fyne.io/fyne/v2/internal/color" - "fyne.io/fyne/v2/internal/driver" + intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" ) @@ -76,14 +75,14 @@ func (r *markupRenderer) setColorAttrWithDefault(attrs map[string]*string, name r.setStringAttr(attrs, name, fmt.Sprintf("rgba(%d,%d,%d,%d)", uint8(rd), uint8(g), uint8(b), uint8(a))) } -func (r *markupRenderer) setFillModeAttr(attrs map[string]*string, name string, m canvas.ImageFill) { +func (r *markupRenderer) setFillModeAttr(attrs map[string]*string, name string, m fynecanvas.ImageFill) { var fillMode string switch m { - case canvas.ImageFillStretch: + case fynecanvas.ImageFillStretch: // default mode, don’t add an attr - case canvas.ImageFillContain: + case fynecanvas.ImageFillContain: fillMode = "contain" - case canvas.ImageFillOriginal: + case fynecanvas.ImageFillOriginal: fillMode = "original" default: fillMode = fmt.Sprintf("unknown fill mode: %d", m) @@ -132,9 +131,10 @@ func (r *markupRenderer) setResourceAttr(attrs map[string]*string, name string, return } + named := false if value := knownResource(rsc); value != "" { r.setStringAttr(attrs, name, value) - return + named = true } var variant string @@ -150,32 +150,28 @@ func (r *markupRenderer) setResourceAttr(attrs map[string]*string, name string, case *theme.ThemedResource: variant = string(t.ColorName) if variant == "" { - variant = "default" - } - case *cache.WidgetResource: - if _, ok := t.ThemedResource.(*theme.InvertedThemedResource); ok { - variant = "inverted" - } else { - variant = string(t.ThemeColorName()) + variant = "foreground" } default: r.setStringAttr(attrs, name, rsc.Name()) return } - // That’s some magic to access the private `source` field of the themed resource. - v := reflect.ValueOf(rsc).Elem().Field(0) - src := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem().Interface().(fyne.Resource) - r.setResourceAttr(attrs, name, src) + if !named { + // That’s some magic to access the private `source` field of the themed resource. + v := reflect.ValueOf(rsc).Elem().Field(0) + src := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem().Interface().(fyne.Resource) + r.setResourceAttr(attrs, name, src) + } r.setStringAttr(attrs, "themed", variant) } -func (r *markupRenderer) setScaleModeAttr(attrs map[string]*string, name string, m canvas.ImageScale) { +func (r *markupRenderer) setScaleModeAttr(attrs map[string]*string, name string, m fynecanvas.ImageScale) { var scaleMode string switch m { - case canvas.ImageScaleSmooth: + case fynecanvas.ImageScaleSmooth: // default mode, don’t add an attr - case canvas.ImageScalePixels: + case fynecanvas.ImageScalePixels: scaleMode = "pixels" default: scaleMode = fmt.Sprintf("unknown scale mode: %d", m) @@ -207,7 +203,7 @@ func (r *markupRenderer) writeCanvas(c fyne.Canvas) { r.writeTag("content", false, nil) r.w.WriteRune('\n') r.indentation++ - driver.WalkVisibleObjectTree(c.Content(), r.writeCanvasObject, r.writeCloseCanvasObject) + intdriver.WalkVisibleObjectTree(c.Content(), r.writeCanvasObject, r.writeCloseCanvasObject) r.indentation-- r.writeIndent() r.writeCloseTag("content") @@ -215,7 +211,7 @@ func (r *markupRenderer) writeCanvas(c fyne.Canvas) { r.writeTag("overlay", false, nil) r.w.WriteRune('\n') r.indentation++ - driver.WalkVisibleObjectTree(o, r.writeCanvasObject, r.writeCloseCanvasObject) + intdriver.WalkVisibleObjectTree(o, r.writeCanvasObject, r.writeCloseCanvasObject) r.indentation-- r.writeIndent() r.writeCloseTag("overlay") @@ -230,21 +226,21 @@ func (r *markupRenderer) writeCanvasObject(obj fyne.CanvasObject, _, _ fyne.Posi r.setPosAttr(attrs, "pos", obj.Position()) r.setSizeAttr(attrs, "size", obj.Size()) switch o := obj.(type) { - case *canvas.Circle: + case *fynecanvas.Circle: r.writeCircle(o, attrs) - case *canvas.Image: + case *fynecanvas.Image: r.writeImage(o, attrs) - case *canvas.Line: + case *fynecanvas.Line: r.writeLine(o, attrs) - case *canvas.LinearGradient: + case *fynecanvas.LinearGradient: r.writeLinearGradient(o, attrs) - case *canvas.RadialGradient: + case *fynecanvas.RadialGradient: r.writeRadialGradient(o, attrs) - case *canvas.Raster: + case *fynecanvas.Raster: r.writeRaster(o, attrs) - case *canvas.Rectangle: + case *fynecanvas.Rectangle: r.writeRectangle(o, attrs) - case *canvas.Text: + case *fynecanvas.Text: r.writeText(o, attrs) case *fyne.Container: r.writeContainer(o, attrs) @@ -259,7 +255,7 @@ func (r *markupRenderer) writeCanvasObject(obj fyne.CanvasObject, _, _ fyne.Posi return false } -func (r *markupRenderer) writeCircle(c *canvas.Circle, attrs map[string]*string) { +func (r *markupRenderer) writeCircle(c *fynecanvas.Circle, attrs map[string]*string) { r.setColorAttr(attrs, "fillColor", c.FillColor) r.setColorAttr(attrs, "strokeColor", c.StrokeColor) r.setFloatAttr(attrs, "strokeWidth", float64(c.StrokeWidth)) @@ -297,7 +293,7 @@ func (r *markupRenderer) writeIndent() { } } -func (r *markupRenderer) writeImage(i *canvas.Image, attrs map[string]*string) { +func (r *markupRenderer) writeImage(i *fynecanvas.Image, attrs map[string]*string) { r.setStringAttr(attrs, "file", i.File) r.setResourceAttr(attrs, "rsc", i.Resource) if i.File == "" && i.Resource == nil { @@ -312,32 +308,32 @@ func (r *markupRenderer) writeImage(i *canvas.Image, attrs map[string]*string) { r.writeTag("image", true, attrs) } -func (r *markupRenderer) writeLine(l *canvas.Line, attrs map[string]*string) { +func (r *markupRenderer) writeLine(l *fynecanvas.Line, attrs map[string]*string) { r.setColorAttr(attrs, "strokeColor", l.StrokeColor) r.setFloatAttrWithDefault(attrs, "strokeWidth", float64(l.StrokeWidth), 1) r.writeTag("line", true, attrs) } -func (r *markupRenderer) writeLinearGradient(g *canvas.LinearGradient, attrs map[string]*string) { +func (r *markupRenderer) writeLinearGradient(g *fynecanvas.LinearGradient, attrs map[string]*string) { r.setColorAttr(attrs, "startColor", g.StartColor) r.setColorAttr(attrs, "endColor", g.EndColor) r.setFloatAttr(attrs, "angle", g.Angle) r.writeTag("linearGradient", true, attrs) } -func (r *markupRenderer) writeRadialGradient(g *canvas.RadialGradient, attrs map[string]*string) { +func (r *markupRenderer) writeRadialGradient(g *fynecanvas.RadialGradient, attrs map[string]*string) { r.setColorAttr(attrs, "startColor", g.StartColor) r.setColorAttr(attrs, "endColor", g.EndColor) r.setFloatPosAttr(attrs, "centerOffset", g.CenterOffsetX, g.CenterOffsetY) r.writeTag("radialGradient", true, attrs) } -func (r *markupRenderer) writeRaster(rst *canvas.Raster, attrs map[string]*string) { +func (r *markupRenderer) writeRaster(rst *fynecanvas.Raster, attrs map[string]*string) { r.setFloatAttr(attrs, "translucency", rst.Translucency) r.writeTag("raster", true, attrs) } -func (r *markupRenderer) writeRectangle(rct *canvas.Rectangle, attrs map[string]*string) { +func (r *markupRenderer) writeRectangle(rct *fynecanvas.Rectangle, attrs map[string]*string) { r.setColorAttr(attrs, "fillColor", rct.FillColor) r.setColorAttr(attrs, "strokeColor", rct.StrokeColor) r.setFloatAttr(attrs, "strokeWidth", float64(rct.StrokeWidth)) @@ -370,7 +366,7 @@ func (r *markupRenderer) writeTag(name string, isEmpty bool, attrs map[string]*s } } -func (r *markupRenderer) writeText(t *canvas.Text, attrs map[string]*string) { +func (r *markupRenderer) writeText(t *fynecanvas.Text, attrs map[string]*string) { r.setColorAttrWithDefault(attrs, "color", t.Color, theme.ForegroundColor()) r.setAlignmentAttr(attrs, "alignment", t.Alignment) r.setSizeAttrWithDefault(attrs, "textSize", t.TextSize, theme.TextSize()) @@ -397,24 +393,30 @@ func nrgbaColor(c color.Color) color.NRGBA { func knownColor(c color.Color) string { return map[color.Color]string{ - nrgbaColor(theme.BackgroundColor()): "background", - nrgbaColor(theme.ButtonColor()): "button", - nrgbaColor(theme.DisabledButtonColor()): "disabled button", - nrgbaColor(theme.DisabledColor()): "disabled", - nrgbaColor(theme.ErrorColor()): "error", - nrgbaColor(theme.FocusColor()): "focus", - nrgbaColor(theme.ForegroundColor()): "foreground", - nrgbaColor(theme.HoverColor()): "hover", - nrgbaColor(theme.InputBackgroundColor()): "inputBackground", - nrgbaColor(theme.InputBorderColor()): "inputBorder", - nrgbaColor(theme.MenuBackgroundColor()): "menuBackground", - nrgbaColor(theme.OnPrimaryColor()): "onPrimary", - nrgbaColor(theme.OverlayBackgroundColor()): "overlayBackground", - nrgbaColor(theme.PlaceHolderColor()): "placeholder", - nrgbaColor(theme.PrimaryColor()): "primary", - nrgbaColor(theme.ScrollBarColor()): "scrollbar", - nrgbaColor(theme.SelectionColor()): "selection", - nrgbaColor(theme.ShadowColor()): "shadow", + nrgbaColor(theme.BackgroundColor()): "background", + nrgbaColor(theme.ButtonColor()): "button", + nrgbaColor(theme.DisabledButtonColor()): "disabled button", + nrgbaColor(theme.DisabledColor()): "disabled", + nrgbaColor(theme.ErrorColor()): "error", + nrgbaColor(theme.FocusColor()): "focus", + nrgbaColor(theme.ForegroundColor()): "foreground", + nrgbaColor(theme.Color(theme.ColorNameHeaderBackground)): "headerBackground", + nrgbaColor(theme.HoverColor()): "hover", + nrgbaColor(theme.Color(theme.ColorNameHyperlink)): "hyperlink", + nrgbaColor(theme.InputBackgroundColor()): "inputBackground", + nrgbaColor(theme.InputBorderColor()): "inputBorder", + nrgbaColor(theme.MenuBackgroundColor()): "menuBackground", + nrgbaColor(theme.Color(theme.ColorNameOnPrimary)): "onPrimary", + nrgbaColor(theme.OverlayBackgroundColor()): "overlayBackground", + nrgbaColor(theme.PlaceHolderColor()): "placeholder", + nrgbaColor(theme.Color(theme.ColorNamePressed)): "pressed", + nrgbaColor(theme.PrimaryColor()): "primary", + nrgbaColor(theme.ScrollBarColor()): "scrollbar", + nrgbaColor(theme.SelectionColor()): "selection", + nrgbaColor(theme.Color(theme.ColorNameSeparator)): "separator", + nrgbaColor(theme.Color(theme.ColorNameSuccess)): "success", + nrgbaColor(theme.ShadowColor()): "shadow", + nrgbaColor(theme.Color(theme.ColorNameWarning)): "warning", }[nrgbaColor(c)] } diff --git a/test/markup_renderer_test.go b/test/markup_renderer_test.go index 72e55eb7bc..d31216ecdc 100644 --- a/test/markup_renderer_test.go +++ b/test/markup_renderer_test.go @@ -8,13 +8,19 @@ import ( "github.com/stretchr/testify/assert" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" + fynecanvas "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) +func Test_knownColor(t *testing.T) { + for _, name := range knownColorNames { + assert.NotEmpty(t, knownColor(theme.Color(name)), "color name %s is not known", name) + } +} + func Test_snapshot(t *testing.T) { // content elements for name, tt := range map[string]struct { @@ -25,7 +31,7 @@ func Test_snapshot(t *testing.T) { want string }{ "circle": { - content: canvas.NewCircle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}), + content: fynecanvas.NewCircle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}), size: fyne.NewSize(42, 42), pos: fyne.NewPos(17, 17), want: "\n" + @@ -35,7 +41,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "circle with theme color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewCircle(theme.ScrollBarColor()), + content: fynecanvas.NewCircle(theme.ScrollBarColor()), want: "\n" + "\t\n" + "\t\t\n" + @@ -43,7 +49,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "circle with named primary color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewCircle(theme.PrimaryColorNamed(theme.ColorPurple)), + content: fynecanvas.NewCircle(theme.PrimaryColorNamed(theme.ColorPurple)), want: "\n" + "\t\n" + "\t\t\n" + @@ -52,7 +58,7 @@ func Test_snapshot(t *testing.T) { }, "circle with stroke": { content: func() fyne.CanvasObject { - c := canvas.NewCircle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}) + c := fynecanvas.NewCircle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}) c.StrokeWidth = 3 c.StrokeColor = theme.BackgroundColor() return c @@ -66,7 +72,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image from file": { - content: canvas.NewImageFromFile("testdata/markup_renderer_image.svg"), + content: fynecanvas.NewImageFromFile("testdata/markup_renderer_image.svg"), size: fyne.NewSize(66, 33), pos: fyne.NewPos(10, 20), want: "\n" + @@ -76,7 +82,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image from image": { - content: canvas.NewImageFromImage(image.NewUniform(color.Transparent)), + content: fynecanvas.NewImageFromImage(image.NewUniform(color.Transparent)), want: "\n" + "\t\n" + "\t\t\n" + @@ -84,7 +90,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image from resource": { - content: canvas.NewImageFromResource(fyne.NewStaticResource("resource name", []byte{})), + content: fynecanvas.NewImageFromResource(fyne.NewStaticResource("resource name", []byte{})), want: "\n" + "\t\n" + "\t\t\n" + @@ -92,23 +98,23 @@ func Test_snapshot(t *testing.T) { "\n", }, "image theme resource": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewImageFromResource(theme.VolumeDownIcon()), + content: fynecanvas.NewImageFromResource(theme.VolumeDownIcon()), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image themed resource": { - content: canvas.NewImageFromResource(theme.NewThemedResource(fyne.NewStaticResource("resource name", []byte{}))), + content: fynecanvas.NewImageFromResource(theme.NewThemedResource(fyne.NewStaticResource("resource name", []byte{}))), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image disabled resource": { - content: canvas.NewImageFromResource(theme.NewDisabledResource(theme.CancelIcon())), + content: fynecanvas.NewImageFromResource(theme.NewDisabledResource(theme.CancelIcon())), want: "\n" + "\t\n" + "\t\t\n" + @@ -116,7 +122,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image error themed resource": { - content: canvas.NewImageFromResource(theme.NewErrorThemedResource(theme.ErrorIcon())), + content: fynecanvas.NewImageFromResource(theme.NewErrorThemedResource(theme.ErrorIcon())), want: "\n" + "\t\n" + "\t\t\n" + @@ -124,7 +130,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image inverted themed resource": { - content: canvas.NewImageFromResource(theme.NewInvertedThemedResource(theme.WarningIcon())), + content: fynecanvas.NewImageFromResource(theme.NewInvertedThemedResource(theme.WarningIcon())), want: "\n" + "\t\n" + "\t\t\n" + @@ -132,7 +138,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "image primary themed resource": { - content: canvas.NewImageFromResource(theme.NewPrimaryThemedResource(theme.VolumeDownIcon())), + content: fynecanvas.NewImageFromResource(theme.NewPrimaryThemedResource(theme.VolumeDownIcon())), want: "\n" + "\t\n" + "\t\t\n" + @@ -141,63 +147,63 @@ func Test_snapshot(t *testing.T) { }, "image with translucency": { content: func() fyne.CanvasObject { - img := canvas.NewImageFromResource(theme.ZoomOutIcon()) + img := fynecanvas.NewImageFromResource(theme.ZoomOutIcon()) img.Translucency = 1.3 return img }(), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image with fill mode contain": { content: func() fyne.CanvasObject { - img := canvas.NewImageFromResource(theme.ZoomOutIcon()) - img.FillMode = canvas.ImageFillContain + img := fynecanvas.NewImageFromResource(theme.ZoomOutIcon()) + img.FillMode = fynecanvas.ImageFillContain return img }(), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image with fill mode original": { content: func() fyne.CanvasObject { - img := canvas.NewImageFromResource(theme.ZoomInIcon()) - img.FillMode = canvas.ImageFillOriginal + img := fynecanvas.NewImageFromResource(theme.ZoomInIcon()) + img.FillMode = fynecanvas.ImageFillOriginal return img }(), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image with scale mode pixels": { content: func() fyne.CanvasObject { - img := canvas.NewImageFromResource(theme.ViewRestoreIcon()) - img.ScaleMode = canvas.ImageScalePixels + img := fynecanvas.NewImageFromResource(theme.ViewRestoreIcon()) + img.ScaleMode = fynecanvas.ImageScalePixels return img }(), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "image with inline icon size": { - content: canvas.NewImageFromResource(theme.VisibilityIcon()), + content: fynecanvas.NewImageFromResource(theme.VisibilityIcon()), size: fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()), want: "\n" + "\t\n" + - "\t\t\n" + + "\t\t\n" + "\t\n" + "\n", }, "line": { - content: canvas.NewLine(color.NRGBA{R: 17, G: 42, B: 128, A: 255}), + content: fynecanvas.NewLine(color.NRGBA{R: 17, G: 42, B: 128, A: 255}), pos: fyne.NewPos(13, 13), size: fyne.NewSize(10, 50), want: "\n" + @@ -207,7 +213,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "line with theme color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewLine(theme.ShadowColor()), + content: fynecanvas.NewLine(theme.ShadowColor()), want: "\n" + "\t\n" + "\t\t\n" + @@ -215,7 +221,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "line with named primary color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewLine(theme.PrimaryColorNamed(theme.ColorBrown)), + content: fynecanvas.NewLine(theme.PrimaryColorNamed(theme.ColorBrown)), want: "\n" + "\t\n" + "\t\t\n" + @@ -224,7 +230,7 @@ func Test_snapshot(t *testing.T) { }, "line with stroke width": { content: func() fyne.CanvasObject { - l := canvas.NewLine(color.NRGBA{R: 17, G: 42, B: 128, A: 255}) + l := fynecanvas.NewLine(color.NRGBA{R: 17, G: 42, B: 128, A: 255}) l.StrokeWidth = 1.125 return l }(), @@ -237,7 +243,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "linear gradient": { - content: canvas.NewLinearGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor(), 13.25), + content: fynecanvas.NewLinearGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor(), 13.25), pos: fyne.NewPos(6, 13), size: fyne.NewSize(50, 10), want: "\n" + @@ -247,7 +253,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "radial gradient": { - content: canvas.NewRadialGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor()), + content: fynecanvas.NewRadialGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor()), want: "\n" + "\t\n" + "\t\t\n" + @@ -256,7 +262,7 @@ func Test_snapshot(t *testing.T) { }, "radial gradient with offset": { content: func() fyne.CanvasObject { - g := canvas.NewRadialGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor()) + g := fynecanvas.NewRadialGradient(color.NRGBA{R: 1, G: 2, B: 3, A: 4}, theme.DisabledColor()) g.CenterOffsetX = 1.5 g.CenterOffsetY = -13.7 return g @@ -268,7 +274,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "raster": { - content: &canvas.Raster{Translucency: 1}, + content: &fynecanvas.Raster{Translucency: 1}, want: "\n" + "\t\n" + "\t\t\n" + @@ -276,7 +282,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "rectangle": { - content: canvas.NewRectangle(color.NRGBA{R: 100, G: 200, B: 50, A: 0}), + content: fynecanvas.NewRectangle(color.NRGBA{R: 100, G: 200, B: 50, A: 0}), size: fyne.NewSize(17, 42), pos: fyne.NewPos(42, 17), want: "\n" + @@ -286,7 +292,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "rectangle with theme color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewRectangle(theme.HoverColor()), + content: fynecanvas.NewRectangle(theme.HoverColor()), want: "\n" + "\t\n" + "\t\t\n" + @@ -294,7 +300,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "rectangle with named primary color": { // we won’t test _all_ valid values … it’s not that important - content: canvas.NewRectangle(theme.PrimaryColorNamed(theme.ColorOrange)), + content: fynecanvas.NewRectangle(theme.PrimaryColorNamed(theme.ColorOrange)), want: "\n" + "\t\n" + "\t\t\n" + @@ -303,7 +309,7 @@ func Test_snapshot(t *testing.T) { }, "rectangle with stroke": { content: func() fyne.CanvasObject { - r := canvas.NewRectangle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}) + r := fynecanvas.NewRectangle(color.NRGBA{R: 200, G: 100, B: 0, A: 50}) r.StrokeWidth = 6.375 r.StrokeColor = theme.PlaceHolderColor() return r @@ -317,7 +323,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "text": { - content: canvas.NewText("foo", color.NRGBA{R: 123, G: 234, B: 56, A: 0}), + content: fynecanvas.NewText("foo", color.NRGBA{R: 123, G: 234, B: 56, A: 0}), size: fyne.NewSize(50, 50), pos: fyne.NewPos(20, 20), want: "\n" + @@ -327,7 +333,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "text with theme color": { - content: canvas.NewText("bar", theme.ForegroundColor()), + content: fynecanvas.NewText("bar", theme.ForegroundColor()), want: "\n" + "\t\n" + "\t\tbar\n" + @@ -335,7 +341,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "text with named primary color": { - content: canvas.NewText("foo", theme.PrimaryColorNamed(theme.ColorYellow)), + content: fynecanvas.NewText("foo", theme.PrimaryColorNamed(theme.ColorYellow)), size: fyne.NewSize(50, 50), pos: fyne.NewPos(20, 20), want: "\n" + @@ -346,7 +352,7 @@ func Test_snapshot(t *testing.T) { }, "text with alignment center": { content: func() fyne.CanvasObject { - t := canvas.NewText("bar", theme.ForegroundColor()) + t := fynecanvas.NewText("bar", theme.ForegroundColor()) t.Alignment = fyne.TextAlignCenter return t }(), @@ -358,7 +364,7 @@ func Test_snapshot(t *testing.T) { }, "text with alignment trailing": { content: func() fyne.CanvasObject { - txt := canvas.NewText("bar", theme.ForegroundColor()) + txt := fynecanvas.NewText("bar", theme.ForegroundColor()) txt.Alignment = fyne.TextAlignTrailing return txt }(), @@ -370,7 +376,7 @@ func Test_snapshot(t *testing.T) { }, "text with size": { content: func() fyne.CanvasObject { - txt := canvas.NewText("big", theme.ForegroundColor()) + txt := fynecanvas.NewText("big", theme.ForegroundColor()) txt.TextSize = 42 return txt }(), @@ -382,7 +388,7 @@ func Test_snapshot(t *testing.T) { }, "text bold": { content: func() fyne.CanvasObject { - txt := canvas.NewText("bold", theme.ForegroundColor()) + txt := fynecanvas.NewText("bold", theme.ForegroundColor()) txt.TextStyle.Bold = true return txt }(), @@ -394,7 +400,7 @@ func Test_snapshot(t *testing.T) { }, "text italic": { content: func() fyne.CanvasObject { - txt := canvas.NewText("italic", theme.ForegroundColor()) + txt := fynecanvas.NewText("italic", theme.ForegroundColor()) txt.TextStyle.Italic = true return txt }(), @@ -406,7 +412,7 @@ func Test_snapshot(t *testing.T) { }, "text monospace": { content: func() fyne.CanvasObject { - txt := canvas.NewText("mono", theme.ForegroundColor()) + txt := fynecanvas.NewText("mono", theme.ForegroundColor()) txt.TextStyle.Monospace = true return txt }(), @@ -417,7 +423,7 @@ func Test_snapshot(t *testing.T) { "\n", }, "container": { - content: container.NewVBox(canvas.NewCircle(color.Black), canvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250})), + content: container.NewVBox(fynecanvas.NewCircle(color.Black), fynecanvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250})), want: "\n" + "\t\n" + "\t\t\n" + @@ -439,8 +445,8 @@ func Test_snapshot(t *testing.T) { "widget with subobjects": { content: &markupRendererTestWidget{ objs: []fyne.CanvasObject{ - canvas.NewCircle(color.Black), - canvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250}), + fynecanvas.NewCircle(color.Black), + fynecanvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250}), }, }, want: "\n" + @@ -462,9 +468,9 @@ func Test_snapshot(t *testing.T) { }, "invisible": { content: func() fyne.CanvasObject { - c := canvas.NewCircle(color.Black) + c := fynecanvas.NewCircle(color.Black) c.Hide() - l := canvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250}) + l := fynecanvas.NewLine(color.NRGBA{R: 250, G: 250, B: 250, A: 250}) l.Hide() w := widget.NewButton("tap me if you can", nil) w.Hide() @@ -496,7 +502,7 @@ func Test_snapshot(t *testing.T) { t.Run("canvas with padding", func(t *testing.T) { c := NewCanvas() c.SetPadded(true) - c.SetContent(canvas.NewCircle(color.Black)) + c.SetContent(fynecanvas.NewCircle(color.Black)) c.Resize(fyne.NewSize(100, 100)) assert.Equal( t, @@ -512,9 +518,9 @@ func Test_snapshot(t *testing.T) { t.Run("canvas with overlays", func(t *testing.T) { c := NewCanvas() c.SetPadded(true) - c.SetContent(canvas.NewCircle(color.Black)) - c.Overlays().Add(canvas.NewRectangle(color.NRGBA{R: 250, G: 250, B: 250, A: 250})) - c.Overlays().Add(canvas.NewRectangle(color.Transparent)) + c.SetContent(fynecanvas.NewCircle(color.Black)) + c.Overlays().Add(fynecanvas.NewRectangle(color.NRGBA{R: 250, G: 250, B: 250, A: 250})) + c.Overlays().Add(fynecanvas.NewRectangle(color.Transparent)) c.Resize(fyne.NewSize(100, 100)) assert.Equal( t, diff --git a/test/notification.go b/test/notification.go index 0e8bc12878..935676c148 100644 --- a/test/notification.go +++ b/test/notification.go @@ -13,8 +13,8 @@ import ( // After the content of f has executed this utility will check that the specified notification was sent. func AssertNotificationSent(t *testing.T, n *fyne.Notification, f func()) { require.NotNil(t, f, "function has to be specified") - require.IsType(t, &testApp{}, fyne.CurrentApp()) - a := fyne.CurrentApp().(*testApp) + require.IsType(t, &app{}, fyne.CurrentApp()) + a := fyne.CurrentApp().(*app) a.lastNotification = nil f() diff --git a/test/test.go b/test/test.go index 6ca12fe8cd..2989d6236b 100644 --- a/test/test.go +++ b/test/test.go @@ -12,7 +12,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/internal/cache" - "fyne.io/fyne/v2/internal/driver" + intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/painter/software" "fyne.io/fyne/v2/internal/test" @@ -134,12 +134,10 @@ func AssertRendersToMarkup(t *testing.T, masterFilename string, c fyne.Canvas, m // deltaX/Y is the dragging distance: <0 for dragging up/left, >0 for dragging down/right. func Drag(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) { matches := func(object fyne.CanvasObject) bool { - if _, ok := object.(fyne.Draggable); ok { - return true - } - return false + _, ok := object.(fyne.Draggable) + return ok } - o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, p, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) if o == nil { return } @@ -153,7 +151,7 @@ func Drag(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) { // FocusNext focuses the next focusable on the canvas. func FocusNext(c fyne.Canvas) { - if tc, ok := c.(*testCanvas); ok { + if tc, ok := c.(*canvas); ok { tc.focusManager().FocusNext() } else { fyne.LogError("FocusNext can only be called with a test canvas", nil) @@ -162,7 +160,7 @@ func FocusNext(c fyne.Canvas) { // FocusPrevious focuses the previous focusable on the canvas. func FocusPrevious(c fyne.Canvas) { - if tc, ok := c.(*testCanvas); ok { + if tc, ok := c.(*canvas); ok { tc.focusManager().FocusPrevious() } else { fyne.LogError("FocusPrevious can only be called with a test canvas", nil) @@ -183,18 +181,16 @@ func MoveMouse(c fyne.Canvas, pos fyne.Position) { return } - tc, _ := c.(*testCanvas) + tc, _ := c.(*canvas) var oldHovered, hovered desktop.Hoverable if tc != nil { oldHovered = tc.hovered } matches := func(object fyne.CanvasObject) bool { - if _, ok := object.(desktop.Hoverable); ok { - return true - } - return false + _, ok := object.(desktop.Hoverable) + return ok } - o, p, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, p, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) if o != nil { hovered = o.(desktop.Hoverable) me := &desktop.MouseEvent{ @@ -223,12 +219,10 @@ func MoveMouse(c fyne.Canvas, pos fyne.Position) { // deltaX/Y is the scrolling distance: <0 for scrolling up/left, >0 for scrolling down/right. func Scroll(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) { matches := func(object fyne.CanvasObject) bool { - if _, ok := object.(fyne.Scrollable); ok { - return true - } - return false + _, ok := object.(fyne.Scrollable) + return ok } - o, _, _ := driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, _, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) if o == nil { return } @@ -291,14 +285,24 @@ func TypeOnCanvas(c fyne.Canvas, chars string) { // ApplyTheme sets the given theme and waits for it to be applied to the current app. func ApplyTheme(t *testing.T, theme fyne.Theme) { - require.IsType(t, &testApp{}, fyne.CurrentApp()) - a := fyne.CurrentApp().(*testApp) + require.IsType(t, &app{}, fyne.CurrentApp()) + a := fyne.CurrentApp().(*app) a.Settings().SetTheme(theme) for a.lastAppliedTheme() != theme { time.Sleep(5 * time.Millisecond) } } +// TempWidgetRenderer allows test scripts to gain access to the current renderer for a widget. +// This can be used for verifying correctness of rendered components for a widget in unit tests. +// The widget renderer is automatically destroyed when the test ends. +// +// Since: 2.5 +func TempWidgetRenderer(t *testing.T, wid fyne.Widget) fyne.WidgetRenderer { + t.Cleanup(func() { cache.DestroyRenderer(wid) }) + return cache.Renderer(wid) +} + // WidgetRenderer allows test scripts to gain access to the current renderer for a widget. // This can be used for verifying correctness of rendered components for a widget in unit tests. func WidgetRenderer(wid fyne.Widget) fyne.WidgetRenderer { @@ -319,7 +323,7 @@ func findTappable(c fyne.Canvas, pos fyne.Position) (o fyne.CanvasObject, p fyne _, ok := object.(fyne.Tappable) return ok } - o, p, _ = driver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + o, p, _ = intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) return } @@ -343,18 +347,15 @@ func handleFocusOnTap(c fyne.Canvas, obj any) { if c == nil { return } - unfocus := true + if focus, ok := obj.(fyne.Focusable); ok { - if dis, ok := obj.(fyne.Disableable); !ok || !dis.Disabled() { - unfocus = false - if focus != c.Focused() { - unfocus = true - } + dis, ok := obj.(fyne.Disableable) + if (!ok || !dis.Disabled()) && focus == c.Focused() { + return } } - if unfocus { - c.Unfocus() - } + + c.Unfocus() } func typeChars(chars []rune, keyDown func(rune)) { diff --git a/test/testtheme.go b/test/testtheme.go deleted file mode 100644 index 424612ea15..0000000000 --- a/test/testtheme.go +++ /dev/null @@ -1,66 +0,0 @@ -package test - -import ( - "image/color" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" -) - -var ( - red = &color.RGBA{R: 200, G: 0, B: 0, A: 255} - green = &color.RGBA{R: 0, G: 255, B: 0, A: 255} - blue = &color.RGBA{R: 0, G: 0, B: 255, A: 255} -) - -// NewTheme returns a new testTheme. -func NewTheme() fyne.Theme { - return &configurableTheme{ - colors: map[fyne.ThemeColorName]color.Color{ - theme.ColorNameBackground: red, - theme.ColorNameButton: color.Black, - theme.ColorNameDisabled: color.Black, - theme.ColorNameDisabledButton: color.White, - theme.ColorNameError: blue, - theme.ColorNameFocus: color.RGBA{red.R, red.G, red.B, 66}, - theme.ColorNameForeground: color.White, - theme.ColorNameHover: green, - theme.ColorNameHeaderBackground: color.RGBA{red.R, red.G, red.B, 22}, - theme.ColorNameInputBackground: color.RGBA{red.R, red.G, red.B, 30}, - theme.ColorNameInputBorder: color.Black, - theme.ColorNameMenuBackground: color.RGBA{red.R, red.G, red.B, 30}, - theme.ColorNameOnPrimary: color.RGBA{red.R, red.G, red.B, 200}, - theme.ColorNameOverlayBackground: color.RGBA{red.R, red.G, red.B, 44}, - theme.ColorNamePlaceHolder: blue, - theme.ColorNamePressed: blue, - theme.ColorNamePrimary: green, - theme.ColorNameScrollBar: blue, - theme.ColorNameSeparator: color.Black, - theme.ColorNameSelection: color.RGBA{red.R, red.G, red.B, 44}, - theme.ColorNameShadow: blue, - }, - fonts: map[fyne.TextStyle]fyne.Resource{ - {}: theme.DefaultTextBoldFont(), - {Bold: true}: theme.DefaultTextItalicFont(), - {Bold: true, Italic: true}: theme.DefaultTextMonospaceFont(), - {Italic: true}: theme.DefaultTextBoldItalicFont(), - {Monospace: true}: theme.DefaultTextFont(), - }, - sizes: map[fyne.ThemeSizeName]float32{ - theme.SizeNameInlineIcon: float32(24), - theme.SizeNameInnerPadding: float32(20), - theme.SizeNameLineSpacing: float32(6), - theme.SizeNamePadding: float32(10), - theme.SizeNameScrollBar: float32(10), - theme.SizeNameScrollBarSmall: float32(2), - theme.SizeNameSeparatorThickness: float32(1), - theme.SizeNameText: float32(18), - theme.SizeNameHeadingText: float32(30.6), - theme.SizeNameSubHeadingText: float32(24), - theme.SizeNameCaptionText: float32(15), - theme.SizeNameInputBorder: float32(5), - theme.SizeNameInputRadius: float32(2), - theme.SizeNameSelectionRadius: float32(6), - }, - } -} diff --git a/test/testwindow.go b/test/testwindow.go deleted file mode 100644 index eeee940941..0000000000 --- a/test/testwindow.go +++ /dev/null @@ -1,142 +0,0 @@ -package test - -import ( - "fyne.io/fyne/v2" -) - -type testWindow struct { - title string - fullScreen bool - fixedSize bool - focused bool - onClosed func() - onCloseIntercepted func() - - canvas *testCanvas - clipboard fyne.Clipboard - driver *testDriver - menu *fyne.MainMenu -} - -// NewWindow creates and registers a new window for test purposes -func NewWindow(content fyne.CanvasObject) fyne.Window { - window := fyne.CurrentApp().NewWindow("") - window.SetContent(content) - return window -} - -func (w *testWindow) Canvas() fyne.Canvas { - return w.canvas -} - -func (w *testWindow) CenterOnScreen() { - // no-op -} - -func (w *testWindow) Clipboard() fyne.Clipboard { - return w.clipboard -} - -func (w *testWindow) Close() { - if w.onClosed != nil { - w.onClosed() - } - w.focused = false - w.driver.removeWindow(w) -} - -func (w *testWindow) Content() fyne.CanvasObject { - return w.Canvas().Content() -} - -func (w *testWindow) FixedSize() bool { - return w.fixedSize -} - -func (w *testWindow) FullScreen() bool { - return w.fullScreen -} - -func (w *testWindow) Hide() { - w.focused = false -} - -func (w *testWindow) Icon() fyne.Resource { - return fyne.CurrentApp().Icon() -} - -func (w *testWindow) MainMenu() *fyne.MainMenu { - return w.menu -} - -func (w *testWindow) Padded() bool { - return w.canvas.Padded() -} - -func (w *testWindow) RequestFocus() { - for _, win := range w.driver.AllWindows() { - win.(*testWindow).focused = false - } - - w.focused = true -} - -func (w *testWindow) Resize(size fyne.Size) { - w.canvas.Resize(size) -} - -func (w *testWindow) SetContent(obj fyne.CanvasObject) { - w.Canvas().SetContent(obj) -} - -func (w *testWindow) SetFixedSize(fixed bool) { - w.fixedSize = fixed -} - -func (w *testWindow) SetIcon(_ fyne.Resource) { - // no-op -} - -func (w *testWindow) SetFullScreen(fullScreen bool) { - w.fullScreen = fullScreen -} - -func (w *testWindow) SetMainMenu(menu *fyne.MainMenu) { - w.menu = menu -} - -func (w *testWindow) SetMaster() { - // no-op -} - -func (w *testWindow) SetOnClosed(closed func()) { - w.onClosed = closed -} - -func (w *testWindow) SetCloseIntercept(callback func()) { - w.onCloseIntercepted = callback -} - -func (w *testWindow) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) { - -} - -func (w *testWindow) SetPadded(padded bool) { - w.canvas.SetPadded(padded) -} - -func (w *testWindow) SetTitle(title string) { - w.title = title -} - -func (w *testWindow) Show() { - w.RequestFocus() -} - -func (w *testWindow) ShowAndRun() { - w.Show() -} - -func (w *testWindow) Title() string { - return w.title -} diff --git a/test/theme.go b/test/theme.go index d37fcfde31..d02a7158c0 100644 --- a/test/theme.go +++ b/test/theme.go @@ -1,6 +1,7 @@ package test import ( + "fmt" "image/color" "fyne.io/fyne/v2" @@ -9,15 +10,77 @@ import ( var defaultTheme fyne.Theme -var _ fyne.Theme = (*configurableTheme)(nil) +// NewTheme returns a new test theme using quiet ugly colors. +func NewTheme() fyne.Theme { + blue := func(alpha uint8) color.Color { + return &color.NRGBA{R: 0, G: 0, B: 255, A: alpha} + } + gray := func(level uint8) color.Color { + return &color.Gray{Y: level} + } + green := func(alpha uint8) color.Color { + return &color.NRGBA{R: 0, G: 255, B: 0, A: alpha} + } + red := func(alpha uint8) color.Color { + return &color.NRGBA{R: 200, G: 0, B: 0, A: alpha} + } -type configurableTheme struct { - colors map[fyne.ThemeColorName]color.Color - fonts map[fyne.TextStyle]fyne.Resource - sizes map[fyne.ThemeSizeName]float32 + return &configurableTheme{ + colors: map[fyne.ThemeColorName]color.Color{ + theme.ColorNameBackground: red(255), + theme.ColorNameButton: gray(100), + theme.ColorNameDisabled: gray(20), + theme.ColorNameDisabledButton: gray(230), + theme.ColorNameError: blue(255), + theme.ColorNameFocus: red(66), + theme.ColorNameForeground: gray(255), + theme.ColorNameHeaderBackground: red(22), + theme.ColorNameHover: green(200), + theme.ColorNameHyperlink: blue(240), + theme.ColorNameInputBackground: red(30), + theme.ColorNameInputBorder: gray(10), + theme.ColorNameMenuBackground: red(50), + theme.ColorNameOnPrimary: red(200), + theme.ColorNameOverlayBackground: red(44), + theme.ColorNamePlaceHolder: blue(200), + theme.ColorNamePressed: blue(250), + theme.ColorNamePrimary: green(255), + theme.ColorNameScrollBar: blue(220), + theme.ColorNameSelection: red(55), + theme.ColorNameSeparator: gray(30), + theme.ColorNameShadow: blue(150), + theme.ColorNameSuccess: green(150), + theme.ColorNameWarning: red(100), + }, + fonts: map[fyne.TextStyle]fyne.Resource{ + {}: theme.DefaultTextBoldFont(), + {Bold: true}: theme.DefaultTextItalicFont(), + {Bold: true, Italic: true}: theme.DefaultTextMonospaceFont(), + {Italic: true}: theme.DefaultTextBoldItalicFont(), + {Monospace: true}: theme.DefaultTextFont(), + {Symbol: true}: theme.DefaultSymbolFont(), + }, + name: "Ugly Test Theme", + sizes: map[fyne.ThemeSizeName]float32{ + theme.SizeNameInlineIcon: float32(24), + theme.SizeNameInnerPadding: float32(20), + theme.SizeNameLineSpacing: float32(6), + theme.SizeNamePadding: float32(10), + theme.SizeNameScrollBar: float32(10), + theme.SizeNameScrollBarSmall: float32(2), + theme.SizeNameSeparatorThickness: float32(1), + theme.SizeNameText: float32(18), + theme.SizeNameHeadingText: float32(30.6), + theme.SizeNameSubHeadingText: float32(24), + theme.SizeNameCaptionText: float32(15), + theme.SizeNameInputBorder: float32(5), + theme.SizeNameInputRadius: float32(2), + theme.SizeNameSelectionRadius: float32(6), + }, + } } -// Theme returns a theme useful for image based tests. +// Theme returns a test theme useful for image based tests. func Theme() fyne.Theme { if defaultTheme == nil { defaultTheme = &configurableTheme{ @@ -29,21 +92,23 @@ func Theme() fyne.Theme { theme.ColorNameError: color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff}, theme.ColorNameFocus: color.NRGBA{R: 0x78, G: 0x3a, B: 0x3a, A: 0xff}, theme.ColorNameForeground: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + theme.ColorNameHeaderBackground: color.NRGBA{R: 0x25, G: 0x25, B: 0x25, A: 0xff}, theme.ColorNameHover: color.NRGBA{R: 0x88, G: 0xff, B: 0xff, A: 0x22}, - theme.ColorNameHeaderBackground: color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff}, theme.ColorNameHyperlink: color.NRGBA{R: 0xff, G: 0xcc, B: 0x80, A: 0xff}, theme.ColorNameInputBackground: color.NRGBA{R: 0x66, G: 0x66, B: 0x66, A: 0xff}, theme.ColorNameInputBorder: color.NRGBA{R: 0x86, G: 0x86, B: 0x86, A: 0xff}, theme.ColorNameMenuBackground: color.NRGBA{R: 0x56, G: 0x56, B: 0x56, A: 0xff}, theme.ColorNameOnPrimary: color.NRGBA{R: 0x08, G: 0x0c, B: 0x0f, A: 0xff}, - theme.ColorNameOverlayBackground: color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff}, + theme.ColorNameOverlayBackground: color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff}, theme.ColorNamePlaceHolder: color.NRGBA{R: 0xaa, G: 0xaa, B: 0xaa, A: 0xff}, - theme.ColorNamePressed: color.NRGBA{A: 0x33}, - theme.ColorNamePrimary: color.NRGBA{R: 0xff, G: 0xcc, B: 0x80, A: 0xff}, + theme.ColorNamePressed: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x33}, + theme.ColorNamePrimary: color.NRGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0xff}, theme.ColorNameScrollBar: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xaa}, - theme.ColorNameSeparator: color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff}, theme.ColorNameSelection: color.NRGBA{R: 0x78, G: 0x3a, B: 0x3a, A: 0x99}, - theme.ColorNameShadow: color.NRGBA{A: 0x88}, + theme.ColorNameSeparator: color.NRGBA{R: 0x90, G: 0x90, B: 0x90, A: 0xff}, + theme.ColorNameShadow: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x88}, + theme.ColorNameSuccess: color.NRGBA{R: 0x00, G: 0x99, B: 0x00, A: 0xff}, + theme.ColorNameWarning: color.NRGBA{R: 0xee, G: 0xee, B: 0x00, A: 0xff}, }, fonts: map[fyne.TextStyle]fyne.Resource{ {}: theme.DefaultTextFont(), @@ -51,7 +116,9 @@ func Theme() fyne.Theme { {Bold: true, Italic: true}: theme.DefaultTextBoldItalicFont(), {Italic: true}: theme.DefaultTextItalicFont(), {Monospace: true}: theme.DefaultTextMonospaceFont(), + {Symbol: true}: theme.DefaultSymbolFont(), }, + name: "Default Test Theme", sizes: map[fyne.ThemeSizeName]float32{ theme.SizeNameInlineIcon: float32(20), theme.SizeNameInnerPadding: float32(8), @@ -73,11 +140,28 @@ func Theme() fyne.Theme { return defaultTheme } +type configurableTheme struct { + colors map[fyne.ThemeColorName]color.Color + fonts map[fyne.TextStyle]fyne.Resource + name string + sizes map[fyne.ThemeSizeName]float32 +} + +var _ fyne.Theme = (*configurableTheme)(nil) + func (t *configurableTheme) Color(n fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { + if t.colors[n] == nil { + fyne.LogError(fmt.Sprintf("color %s not defined in theme %s", n, t.name), nil) + } + return t.colors[n] } func (t *configurableTheme) Font(style fyne.TextStyle) fyne.Resource { + if t.fonts[style] == nil { + fyne.LogError(fmt.Sprintf("font for style %#v not defined in theme %s", style, t.name), nil) + } + return t.fonts[style] } @@ -86,5 +170,9 @@ func (t *configurableTheme) Icon(n fyne.ThemeIconName) fyne.Resource { } func (t *configurableTheme) Size(s fyne.ThemeSizeName) float32 { + if t.sizes[s] == 0 { + fyne.LogError(fmt.Sprintf("size %s not defined in theme %s", s, t.name), nil) + } + return t.sizes[s] } diff --git a/test/theme_test.go b/test/theme_test.go new file mode 100644 index 0000000000..603eece7ce --- /dev/null +++ b/test/theme_test.go @@ -0,0 +1,96 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Try to keep these in sync with the existing color names at theme/color.go. +var knownColorNames = []fyne.ThemeColorName{ + theme.ColorNameBackground, + theme.ColorNameButton, + theme.ColorNameDisabledButton, + theme.ColorNameDisabled, + theme.ColorNameError, + theme.ColorNameFocus, + theme.ColorNameForeground, + theme.ColorNameHeaderBackground, + theme.ColorNameHover, + theme.ColorNameHyperlink, + theme.ColorNameInputBackground, + theme.ColorNameInputBorder, + theme.ColorNameMenuBackground, + theme.ColorNameOnPrimary, + theme.ColorNameOverlayBackground, + theme.ColorNamePlaceHolder, + theme.ColorNamePressed, + theme.ColorNamePrimary, + theme.ColorNameScrollBar, + theme.ColorNameSelection, + theme.ColorNameSeparator, + theme.ColorNameShadow, + theme.ColorNameSuccess, + theme.ColorNameWarning, +} + +// Try to keep this in sync with the existing variants at theme/theme.go +var knownVariants = []fyne.ThemeVariant{ + theme.VariantDark, + theme.VariantLight, +} + +func Test_NewTheme(t *testing.T) { + suite.Run(t, &configurableThemeTestSuite{ + constructor: NewTheme, + name: "Ugly Test Theme", + }) +} + +func Test_Theme(t *testing.T) { + suite.Run(t, &configurableThemeTestSuite{ + constructor: Theme, + name: "Default Test Theme", + }) +} + +type configurableThemeTestSuite struct { + suite.Suite + constructor func() fyne.Theme + name string +} + +func (s *configurableThemeTestSuite) TestAllColorsDefined() { + t := s.T() + th := s.constructor() + for _, variant := range knownVariants { + for _, cn := range knownColorNames { + assert.NotNil(t, th.Color(cn, variant), "undefined color %s variant %d in theme %s", cn, variant, s.name) + } + } +} + +func (s *configurableThemeTestSuite) TestUniqueColorValues() { + t := s.T() + th := s.constructor() + seenByVariant := map[fyne.ThemeVariant]map[string]fyne.ThemeColorName{} + for _, variant := range knownVariants { + seen := seenByVariant[variant] + if seen == nil { + seen = map[string]fyne.ThemeColorName{} + seenByVariant[variant] = seen + } + for _, cn := range knownColorNames { + c := th.Color(cn, theme.VariantDark) + r, g, b, a := c.RGBA() + key := fmt.Sprintf("%d %d %d %d", r, g, b, a) + assert.True(t, seen[key] == "", "color value %#v for color %s variant %d already used for color %s in theme %s", c, cn, variant, seen[key], s.name) + seen[key] = cn + } + } +} diff --git a/test/window.go b/test/window.go new file mode 100644 index 0000000000..17bba3b92c --- /dev/null +++ b/test/window.go @@ -0,0 +1,142 @@ +package test + +import ( + "fyne.io/fyne/v2" +) + +type window struct { + title string + fullScreen bool + fixedSize bool + focused bool + onClosed func() + onCloseIntercepted func() + + canvas *canvas + clipboard clipboard + driver *driver + menu *fyne.MainMenu +} + +// NewWindow creates and registers a new window for test purposes +func NewWindow(content fyne.CanvasObject) fyne.Window { + window := fyne.CurrentApp().NewWindow("") + window.SetContent(content) + return window +} + +func (w *window) Canvas() fyne.Canvas { + return w.canvas +} + +func (w *window) CenterOnScreen() { + // no-op +} + +func (w *window) Clipboard() fyne.Clipboard { + return &w.clipboard +} + +func (w *window) Close() { + if w.onClosed != nil { + w.onClosed() + } + w.focused = false + w.driver.removeWindow(w) +} + +func (w *window) Content() fyne.CanvasObject { + return w.Canvas().Content() +} + +func (w *window) FixedSize() bool { + return w.fixedSize +} + +func (w *window) FullScreen() bool { + return w.fullScreen +} + +func (w *window) Hide() { + w.focused = false +} + +func (w *window) Icon() fyne.Resource { + return fyne.CurrentApp().Icon() +} + +func (w *window) MainMenu() *fyne.MainMenu { + return w.menu +} + +func (w *window) Padded() bool { + return w.canvas.Padded() +} + +func (w *window) RequestFocus() { + for _, win := range w.driver.AllWindows() { + win.(*window).focused = false + } + + w.focused = true +} + +func (w *window) Resize(size fyne.Size) { + w.canvas.Resize(size) +} + +func (w *window) SetContent(obj fyne.CanvasObject) { + w.Canvas().SetContent(obj) +} + +func (w *window) SetFixedSize(fixed bool) { + w.fixedSize = fixed +} + +func (w *window) SetIcon(_ fyne.Resource) { + // no-op +} + +func (w *window) SetFullScreen(fullScreen bool) { + w.fullScreen = fullScreen +} + +func (w *window) SetMainMenu(menu *fyne.MainMenu) { + w.menu = menu +} + +func (w *window) SetMaster() { + // no-op +} + +func (w *window) SetOnClosed(closed func()) { + w.onClosed = closed +} + +func (w *window) SetCloseIntercept(callback func()) { + w.onCloseIntercepted = callback +} + +func (w *window) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) { + +} + +func (w *window) SetPadded(padded bool) { + w.canvas.SetPadded(padded) +} + +func (w *window) SetTitle(title string) { + w.title = title +} + +func (w *window) Show() { + w.RequestFocus() +} + +func (w *window) ShowAndRun() { + w.Show() +} + +func (w *window) Title() string { + return w.title +} diff --git a/text.go b/text.go index a4cf8f0c18..3c8b6df12f 100644 --- a/text.go +++ b/text.go @@ -61,10 +61,14 @@ type TextStyle struct { Symbol bool // Use the system symbol font. // Since: 2.1 TabWidth int // Width of tabs in spaces + // Since: 2.5 + // Currently only supported by the TextGrid widget. + Underline bool // Should text be underlined. } // MeasureText uses the current driver to calculate the size of text when rendered. +// The font used will be read from the current app's theme. func MeasureText(text string, size float32, style TextStyle) Size { - s, _ := CurrentApp().Driver().RenderedTextSize(text, size, style) + s, _ := CurrentApp().Driver().RenderedTextSize(text, size, style, nil) return s } diff --git a/theme/color.go b/theme/color.go index 7481aa6d6c..f192c161b0 100644 --- a/theme/color.go +++ b/theme/color.go @@ -6,6 +6,7 @@ import ( "fyne.io/fyne/v2" ) +// Keep in mind to add new constants to the tests at test/theme_test.go. const ( // ColorRed is the red primary color name. // @@ -268,13 +269,6 @@ func MenuBackgroundColor() color.Color { return safeColorLookup(ColorNameMenuBackground, currentVariant()) } -// OnPrimaryColor returns the color used for text and icons against the PrimaryColor. -// -// Since: 2.5 -func OnPrimaryColor() color.Color { - return safeColorLookup(ColorNameOnPrimary, currentVariant()) -} - // OnPrimaryColorNamed returns a theme specific color used for text and icons against the named primary color. // // Since: 2.5 diff --git a/theme/color_test.go b/theme/color_test.go index 3bab833061..d7af1f21fd 100644 --- a/theme/color_test.go +++ b/theme/color_test.go @@ -45,11 +45,6 @@ func Test_HoverColor(t *testing.T) { assert.Equal(t, theme.DarkTheme().Color(theme.ColorNameHover, theme.VariantDark), c, "wrong hover color") } -func Test_OnPrimaryColor(t *testing.T) { - fyne.CurrentApp().Settings().SetTheme(theme.DefaultTheme()) - assert.Equal(t, theme.DefaultTheme().Color(theme.ColorNameOnPrimary, fyne.CurrentApp().Settings().ThemeVariant()), theme.OnPrimaryColor(), "wrong primary color") -} - func Test_PlaceHolderColor(t *testing.T) { fyne.CurrentApp().Settings().SetTheme(theme.DarkTheme()) c := theme.PlaceHolderColor() diff --git a/theme/theme.go b/theme/theme.go index 424746c3d6..6968f51f9d 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne/v2/internal/cache" ) +// Keep in mind to add new constants to the tests at test/theme_test.go. const ( // VariantDark is the version of a theme that satisfies a user preference for a dark look. // @@ -195,7 +196,7 @@ func Current() fyne.Theme { // It looks for widget overrides and falls back to the application's current theme. // // Since: 2.5 -func CurrentForWidget(w fyne.Widget) fyne.Theme { +func CurrentForWidget(w fyne.CanvasObject) fyne.Theme { if custom := cache.WidgetTheme(w); custom != nil { return custom } diff --git a/widget.go b/widget.go index cfc66b1cb5..07741aa976 100644 --- a/widget.go +++ b/widget.go @@ -15,7 +15,10 @@ type Widget interface { // This is returned from a widget's declarative object through the CreateRenderer() // function and should be exactly one instance per widget in memory. type WidgetRenderer interface { - // Destroy is for internal use. + // 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. + // 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. diff --git a/widget/accordion.go b/widget/accordion.go index 47ed7dab1e..a904ea55f8 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -234,10 +234,10 @@ func (r *accordionRenderer) Refresh() { } func (r *accordionRenderer) updateObjects() { - th := r.container.themeWithLock() r.container.propertyLock.RLock() defer r.container.propertyLock.RUnlock() + th := r.container.themeWithLock() is := len(r.container.Items) hs := len(r.headers) ds := len(r.dividers) diff --git a/widget/accordion_internal_test.go b/widget/accordion_internal_test.go index 4633d9e984..db83052ad7 100644 --- a/widget/accordion_internal_test.go +++ b/widget/accordion_internal_test.go @@ -13,7 +13,7 @@ import ( func TestAccordion_Toggle(t *testing.T) { ai := NewAccordionItem("foo", NewLabel("foobar")) ac := NewAccordion(ai) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) aih := ar.headers[0] assert.False(t, ai.Open) @@ -35,7 +35,7 @@ func TestAccordionRenderer_Layout(t *testing.T) { ac.Append(ai1) ac.Append(ai2) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) aih0 := ar.headers[0] aih1 := ar.headers[1] aih2 := ar.headers[2] @@ -143,7 +143,7 @@ func TestAccordionRenderer_Layout(t *testing.T) { func TestAccordionRenderer_MinSize(t *testing.T) { t.Run("Empty", func(t *testing.T) { ac := NewAccordion() - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() assert.Equal(t, float32(0), min.Width) assert.Equal(t, float32(0), min.Height) @@ -154,7 +154,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac := NewAccordion() ac.Append(ai) ac.Open(0) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih := ar.headers[0].MinSize() aid := ai.Detail.MinSize() @@ -165,7 +165,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac := NewAccordion() ac.Append(ai) ac.Close(0) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih := ar.headers[0].MinSize() assert.Equal(t, aih.Width, min.Width) @@ -184,7 +184,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac.Open(0) ac.Close(1) ac.Close(2) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih0 := ar.headers[0].MinSize() aih1 := ar.headers[1].MinSize() @@ -209,7 +209,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac.Append(ai1) ac.Append(ai2) ac.OpenAll() - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih0 := ar.headers[0].MinSize() aih1 := ar.headers[1].MinSize() @@ -242,7 +242,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac.Open(0) ac.Open(1) ac.Close(2) - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih0 := ar.headers[0].MinSize() aih1 := ar.headers[1].MinSize() @@ -268,7 +268,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { ac.Append(ai1) ac.Append(ai2) ac.CloseAll() - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) min := ar.MinSize() aih0 := ar.headers[0].MinSize() aih1 := ar.headers[1].MinSize() @@ -287,7 +287,7 @@ func TestAccordionRenderer_MinSize(t *testing.T) { func TestAccordionRenderer_AddRemove(t *testing.T) { ac := NewAccordion() - ar := test.WidgetRenderer(ac).(*accordionRenderer) + ar := test.TempWidgetRenderer(t, ac).(*accordionRenderer) ac.Append(NewAccordionItem("foo0", NewLabel("foobar0"))) ac.Append(NewAccordionItem("foo1", NewLabel("foobar1"))) ac.Append(NewAccordionItem("foo2", NewLabel("foobar2"))) diff --git a/widget/activity.go b/widget/activity.go index 1bfa3eb584..412cd9fdc4 100644 --- a/widget/activity.go +++ b/widget/activity.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/theme" ) @@ -43,9 +42,7 @@ func (a *Activity) Start() { return // already started } - if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok { - r.start() - } + a.Refresh() } // Stop the activity indicator animation @@ -54,9 +51,7 @@ func (a *Activity) Stop() { return // already stopped } - if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok { - r.stop() - } + a.Refresh() } func (a *Activity) CreateRenderer() fyne.WidgetRenderer { @@ -86,12 +81,14 @@ type activityRenderer struct { dots []fyne.CanvasObject parent *Activity - bound fyne.Size - maxCol color.NRGBA - maxRad float32 + bound fyne.Size + maxCol color.NRGBA + maxRad float32 + wasStarted bool } func (a *activityRenderer) Destroy() { + a.parent.started.Store(false) a.stop() } @@ -109,6 +106,17 @@ func (a *activityRenderer) Objects() []fyne.CanvasObject { } func (a *activityRenderer) Refresh() { + started := a.parent.started.Load() + if started { + if !a.wasStarted { + a.start() + } + return + } else if a.wasStarted { + a.stop() + return + } + a.updateColor() } @@ -152,10 +160,12 @@ func (a *activityRenderer) scaleDot(dot *canvas.Circle, off float32) { } func (a *activityRenderer) start() { + a.wasStarted = true a.anim.Start() } func (a *activityRenderer) stop() { + a.wasStarted = false a.anim.Stop() } diff --git a/widget/activity_internal_test.go b/widget/activity_internal_test.go index 1d5a90195a..23582d3d09 100644 --- a/widget/activity_internal_test.go +++ b/widget/activity_internal_test.go @@ -16,7 +16,7 @@ func TestActivity_Animation(t *testing.T) { defer w.Close() w.Resize(a.MinSize()) - render := test.WidgetRenderer(a).(*activityRenderer) + render := test.TempWidgetRenderer(t, a).(*activityRenderer) render.anim.Tick(0) test.AssertImageMatches(t, "activity/animate_0.0.png", w.Canvas().Capture()) diff --git a/widget/button.go b/widget/button.go index 13280ee8a2..88601fabb8 100644 --- a/widget/button.go +++ b/widget/button.go @@ -6,7 +6,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/internal/cache" col "fyne.io/fyne/v2/internal/color" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/layout" @@ -315,7 +314,7 @@ func (r *buttonRenderer) Refresh() { } // applyTheme updates this button to match the current theme -// must be called with the button propertyLock held +// must be called with the button propertyLock RLocked func (r *buttonRenderer) applyTheme() { th := r.button.themeWithLock() v := fyne.CurrentApp().Settings().ThemeVariant() @@ -337,12 +336,14 @@ func (r *buttonRenderer) applyTheme() { r.label.Refresh() if r.icon != nil && r.icon.Resource != nil { icon := r.icon.Resource - if thRes, ok := icon.(fyne.ThemedResource); ok { - if thRes.ThemeColorName() != fgColorName { - icon = theme.NewColoredResource(icon, fgColorName) + if r.button.Importance != MediumImportance { + if thRes, ok := icon.(fyne.ThemedResource); ok { + if thRes.ThemeColorName() != fgColorName { + icon = theme.NewColoredResource(icon, fgColorName) + } } } - r.icon.Resource = cache.OverrideResourceTheme(icon, r.button) + r.icon.Resource = icon r.icon.Refresh() } } @@ -389,7 +390,7 @@ func (r *buttonRenderer) padding(th fyne.Theme) fyne.Size { return fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding) * 2) } -// must be called with r.button.propertyLock held +// must be called with r.button.propertyLock RLocked func (r *buttonRenderer) updateIconAndText() { if r.button.Icon != nil && r.button.Visible() { icon := r.button.Icon @@ -401,7 +402,7 @@ func (r *buttonRenderer) updateIconAndText() { if r.button.Disabled() { icon = theme.NewDisabledResource(icon) } - r.icon.Resource = cache.OverrideResourceTheme(icon, r.button) + r.icon.Resource = icon r.icon.Refresh() r.icon.Show() } else if r.icon != nil { diff --git a/widget/button_internal_test.go b/widget/button_internal_test.go index b70a9de3d6..ba86b6411c 100644 --- a/widget/button_internal_test.go +++ b/widget/button_internal_test.go @@ -18,7 +18,7 @@ import ( func TestButton_Style(t *testing.T) { button := NewButton("Test", nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) render.applyTheme() bg := render.background.FillColor @@ -29,7 +29,7 @@ func TestButton_Style(t *testing.T) { func TestButton_DisabledColor(t *testing.T) { button := NewButton("Test", nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) render.applyTheme() bg := render.background.FillColor button.Importance = MediumImportance @@ -43,7 +43,7 @@ func TestButton_DisabledColor(t *testing.T) { func TestButton_Hover_Math(t *testing.T) { button := NewButtonWithIcon("Test", theme.HomeIcon(), func() {}) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) button.hovered = true // unpremultiplied over operator: // outA = srcA + dstA*(1-srcA) @@ -82,7 +82,7 @@ func TestButton_Hover_Math(t *testing.T) { func TestButton_DisabledIcon(t *testing.T) { button := NewButtonWithIcon("Test", theme.CancelIcon(), nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) assert.Equal(t, render.icon.Resource.Name(), theme.CancelIcon().Name()) button.Disable() @@ -94,7 +94,7 @@ func TestButton_DisabledIcon(t *testing.T) { func TestButton_DisabledIconChangeUsingSetIcon(t *testing.T) { button := NewButtonWithIcon("Test", theme.CancelIcon(), nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) assert.Equal(t, render.icon.Resource.Name(), theme.CancelIcon().Name()) // assert we are using the disabled original icon @@ -116,7 +116,7 @@ func TestButton_DisabledIconChangeUsingSetIcon(t *testing.T) { func TestButton_DisabledIconChangedDirectly(t *testing.T) { button := NewButtonWithIcon("Test", theme.CancelIcon(), nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) assert.Equal(t, render.icon.Resource.Name(), theme.CancelIcon().Name()) // assert we are using the disabled original icon @@ -142,7 +142,7 @@ func TestButton_Focus(t *testing.T) { button := NewButton("Test", func() { tapped = true }) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) render.applyTheme() assert.Equal(t, theme.ButtonColor(), render.background.FillColor) @@ -161,7 +161,7 @@ func TestButton_Focus(t *testing.T) { func TestButtonRenderer_Layout(t *testing.T) { button := NewButtonWithIcon("Test", theme.CancelIcon(), nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) render.Layout(render.MinSize()) assert.True(t, render.icon.Position().X < render.label.Position().X) @@ -172,7 +172,7 @@ func TestButtonRenderer_Layout(t *testing.T) { func TestButtonRenderer_Layout_Stretch(t *testing.T) { button := NewButtonWithIcon("Test", theme.CancelIcon(), nil) button.Resize(button.MinSize().Add(fyne.NewSize(100, 100))) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) textHeight := render.label.MinSize().Height minIconHeight := fyne.Max(theme.IconInlineSize(), textHeight) @@ -186,7 +186,7 @@ func TestButtonRenderer_Layout_Stretch(t *testing.T) { func TestButtonRenderer_Layout_NoText(t *testing.T) { button := NewButtonWithIcon("", theme.CancelIcon(), nil) - render := test.WidgetRenderer(button).(*buttonRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) button.Resize(fyne.NewSize(100, 100)) @@ -196,8 +196,8 @@ func TestButtonRenderer_Layout_NoText(t *testing.T) { func TestButtonRenderer_ApplyTheme(t *testing.T) { button := &Button{} - render := test.WidgetRenderer(button).(*buttonRenderer) - textRender := test.WidgetRenderer(render.label).(*textRenderer) + render := test.TempWidgetRenderer(t, button).(*buttonRenderer) + textRender := test.TempWidgetRenderer(t, render.label).(*textRenderer) textSize := textRender.Objects()[0].(*canvas.Text).TextSize customTextSize := textSize @@ -219,7 +219,7 @@ func TestButtonRenderer_TapAnimation(t *testing.T) { w.Resize(fyne.NewSize(50, 50).Add(fyne.NewSize(20, 20))) button.Resize(fyne.NewSize(50, 50)) - render1 := test.WidgetRenderer(button).(*buttonRenderer) + render1 := test.TempWidgetRenderer(t, button).(*buttonRenderer) test.Tap(button) button.tapAnim.Tick(0.5) test.AssertImageMatches(t, "button/tap_animation.png", w.Canvas().Capture()) @@ -227,7 +227,7 @@ func TestButtonRenderer_TapAnimation(t *testing.T) { cache.DestroyRenderer(button) button.Refresh() - render2 := test.WidgetRenderer(button).(*buttonRenderer) + render2 := test.TempWidgetRenderer(t, button).(*buttonRenderer) assert.NotEqual(t, render1, render2) diff --git a/widget/card_test.go b/widget/card_test.go index 1765040a63..2e914e165d 100644 --- a/widget/card_test.go +++ b/widget/card_test.go @@ -15,7 +15,7 @@ import ( func TestCard_SetImage(t *testing.T) { c := widget.NewCard("Title", "sub", widget.NewLabel("Content")) - r := test.WidgetRenderer(c) + r := test.TempWidgetRenderer(t, c) assert.Equal(t, 4, len(r.Objects())) // the 3 above plus shadow c.SetImage(canvas.NewImageFromResource(theme.ComputerIcon())) @@ -24,7 +24,7 @@ func TestCard_SetImage(t *testing.T) { func TestCard_SetContent(t *testing.T) { c := widget.NewCard("Title", "sub", widget.NewLabel("Content")) - r := test.WidgetRenderer(c) + r := test.TempWidgetRenderer(t, c) assert.Equal(t, 4, len(r.Objects())) // the 3 above plus shadow newContent := widget.NewLabel("New") diff --git a/widget/check_internal_test.go b/widget/check_internal_test.go index 9eec2a0984..49014b0fd7 100644 --- a/widget/check_internal_test.go +++ b/widget/check_internal_test.go @@ -45,7 +45,7 @@ func TestCheckUnChecked(t *testing.T) { func TestCheck_DisabledWhenChecked(t *testing.T) { check := NewCheck("Hi", nil) check.SetChecked(true) - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) assert.True(t, strings.HasPrefix(render.icon.Resource.Name(), "primary_")) @@ -55,7 +55,7 @@ func TestCheck_DisabledWhenChecked(t *testing.T) { func TestCheck_DisabledWhenUnchecked(t *testing.T) { check := NewCheck("Hi", nil) - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) assert.True(t, strings.HasPrefix(render.icon.Resource.Name(), "inputBorder_")) check.Disable() @@ -129,7 +129,7 @@ func TestCheck_Focused(t *testing.T) { check := NewCheck("Test", func(on bool) {}) w := test.NewWindow(check) defer w.Close() - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) assert.False(t, check.focused) assert.Equal(t, color.Transparent, render.focusIndicator.FillColor) @@ -168,7 +168,7 @@ func TestCheck_Hovered(t *testing.T) { check := NewCheck("Test", func(on bool) {}) w := test.NewWindow(check) defer w.Close() - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) check.SetChecked(true) assert.False(t, check.hovered) @@ -214,7 +214,7 @@ func TestCheck_HoveredOutsideActiveArea(t *testing.T) { check := NewCheck("Test", func(on bool) {}) w := test.NewWindow(check) defer w.Close() - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) check.SetChecked(true) assert.False(t, check.hovered) @@ -274,7 +274,7 @@ func TestCheck_Disabled(t *testing.T) { func TestCheckRenderer_ApplyTheme(t *testing.T) { check := &Check{} v := fyne.CurrentApp().Settings().ThemeVariant() - render := test.WidgetRenderer(check).(*checkRenderer) + render := test.TempWidgetRenderer(t, check).(*checkRenderer) textSize := render.label.TextSize customTextSize := textSize diff --git a/widget/entry.go b/widget/entry.go index 3cfe999936..46bada3c3e 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -243,7 +243,7 @@ func (e *Entry) DoubleTapped(p *fyne.PointEvent) { return } - e.SetFieldsAndRefresh(func() { + e.setFieldsAndRefresh(func() { if !e.selectKeyDown { e.selectRow = e.CursorRow e.selectColumn = start @@ -316,7 +316,7 @@ func (e *Entry) ExtendBaseWidget(wid fyne.Widget) { // // Implements: fyne.Focusable func (e *Entry) FocusGained() { - e.SetFieldsAndRefresh(func() { + e.setFieldsAndRefresh(func() { e.dirty = true e.focused = true }) @@ -329,7 +329,7 @@ func (e *Entry) FocusGained() { // // Implements: fyne.Focusable func (e *Entry) FocusLost() { - e.SetFieldsAndRefresh(func() { + e.setFieldsAndRefresh(func() { e.focused = false e.selectKeyDown = false }) @@ -877,6 +877,37 @@ func (e *Entry) typedKeyEnd(provider *RichText) { e.propertyLock.Unlock() } +// handler for Ctrl+[backspace/delete] - delete the word +// to the left or right of the cursor +func (e *Entry) deleteWord(right bool) { + provider := e.textProvider() + cursorRow, cursorCol := e.CursorRow, e.CursorColumn + + // start, end relative to text row + start, end := getTextWhitespaceRegion(provider.row(cursorRow), cursorCol, true) + if right { + start = cursorCol + } else { + end = cursorCol + } + if start == -1 || end == -1 { + return + } + + // convert start, end to absolute text position + b := provider.rowBoundary(cursorRow) + if b != nil { + start += b.begin + end += b.begin + } + + provider.deleteFromTo(start, end) + if !right { + e.CursorColumn = cursorCol - (end - start) + } + e.updateTextAndRefresh(provider.String(), false) +} + func (e *Entry) typedKeyTab() { if dd, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok { if dd.CurrentKeyModifiers()&fyne.KeyModifierShift != 0 { @@ -1154,7 +1185,7 @@ func (e *Entry) registerShortcut() { return } - e.SetFieldsAndRefresh(func() { + e.setFieldsAndRefresh(func() { if s.(*desktop.CustomShortcut).KeyName == fyne.KeyLeft { if e.CursorColumn == 0 { if e.CursorRow > 0 { @@ -1214,6 +1245,11 @@ func (e *Entry) registerShortcut() { e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: moveWordModifier | fyne.KeyModifierShift}, selectMoveWord) e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: moveWordModifier}, unselectMoveWord) e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: moveWordModifier | fyne.KeyModifierShift}, selectMoveWord) + + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyBackspace, Modifier: moveWordModifier}, + func(fyne.Shortcut) { e.deleteWord(false) }) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyDelete, Modifier: moveWordModifier}, + func(fyne.Shortcut) { e.deleteWord(true) }) } func (e *Entry) requestFocus() { @@ -1255,7 +1291,7 @@ func (e *Entry) selectAll() { if e.textProvider().len() == 0 { return } - e.SetFieldsAndRefresh(func() { + e.setFieldsAndRefresh(func() { e.selectRow = 0 e.selectColumn = 0 @@ -1303,7 +1339,7 @@ func (e *Entry) selectingKeyHandler(key *fyne.KeyEvent) bool { case fyne.KeyReturn, fyne.KeyEnter: if e.MultiLine { // clear the selection -- return unhandled to add the newline - e.SetFieldsAndRefresh(e.eraseSelectionAndUpdate) + e.setFieldsAndRefresh(e.eraseSelectionAndUpdate) } return false } @@ -1603,6 +1639,18 @@ func (e *Entry) selectCurrentRow() { e.Refresh() } +func (e *Entry) setFieldsAndRefresh(f func()) { + e.propertyLock.Lock() + f() + e.propertyLock.Unlock() + + impl := e.super() + if impl == nil { + return + } + impl.Refresh() +} + var _ fyne.WidgetRenderer = (*entryRenderer)(nil) type entryRenderer struct { @@ -1694,7 +1742,7 @@ func (r *entryRenderer) Layout(size fyne.Size) { resizedTextPos := r.entry.textPosFromRowCol(r.entry.CursorRow, r.entry.CursorColumn) r.entry.propertyLock.Unlock() if textPos != resizedTextPos { - r.entry.SetFieldsAndRefresh(func() { + r.entry.setFieldsAndRefresh(func() { r.entry.CursorRow, r.entry.CursorColumn = r.entry.rowColFromTextPos(textPos) if r.entry.selecting { @@ -2189,7 +2237,10 @@ func getTextWhitespaceRegion(row []rune, col int, expand bool) (int, int) { // IndexByte will find the position of the next unwanted character, this is to be the end // marker for the selection - end := strings.IndexByte(toks[endCheck:], c) + end := -1 + if endCheck != -1 { + end = strings.IndexByte(toks[endCheck:], c) + } if end == -1 { end = len(toks) // snap end to len(toks) if it results in -1 diff --git a/widget/entry_password.go b/widget/entry_password.go index 912d11b594..ff8f75d4c3 100644 --- a/widget/entry_password.go +++ b/widget/entry_password.go @@ -45,7 +45,7 @@ func (r *passwordRevealer) Tapped(*fyne.PointEvent) { return } - r.entry.SetFieldsAndRefresh(func() { + r.entry.setFieldsAndRefresh(func() { r.entry.Password = !r.entry.Password }) fyne.CurrentApp().Driver().CanvasForObject(r).Focus(r.entry.super().(fyne.Focusable)) diff --git a/widget/entry_password_extend_test.go b/widget/entry_password_extend_test.go index 9173e5c382..558a4ddbb8 100644 --- a/widget/entry_password_extend_test.go +++ b/widget/entry_password_extend_test.go @@ -21,19 +21,19 @@ func TestEntry_Password_Extended_CreateRenderer(t *testing.T) { entry.ExtendBaseWidget(entry) entry.Password = true entry.Wrapping = fyne.TextWrap(fyne.TextTruncateClip) - assert.NotNil(t, test.WidgetRenderer(entry)) - r := test.WidgetRenderer(entry).(*entryRenderer).scroll.Content.(*entryContent) - p := test.WidgetRenderer(r).(*entryContentRenderer).provider + assert.NotNil(t, test.TempWidgetRenderer(t, entry)) + r := test.TempWidgetRenderer(t, entry).(*entryRenderer).scroll.Content.(*entryContent) + p := test.TempWidgetRenderer(t, r).(*entryContentRenderer).provider w.SetContent(entry) test.Type(entry, "Pass") - texts := test.WidgetRenderer(p).(*textRenderer).Objects() + texts := test.TempWidgetRenderer(t, p).(*textRenderer).Objects() assert.Equal(t, passwordChar+passwordChar+passwordChar+passwordChar, texts[0].(*canvas.Text).Text) assert.NotNil(t, entry.ActionItem) test.Tap(entry.ActionItem.(*passwordRevealer)) - texts = test.WidgetRenderer(p).(*textRenderer).Objects() + texts = test.TempWidgetRenderer(t, p).(*textRenderer).Objects() assert.Equal(t, "Pass", texts[0].(*canvas.Text).Text) assert.Equal(t, entry, w.Canvas().Focused()) } diff --git a/widget/entry_password_test.go b/widget/entry_password_test.go index 10742737fa..94a7b3659b 100644 --- a/widget/entry_password_test.go +++ b/widget/entry_password_test.go @@ -14,12 +14,12 @@ import ( func TestNewPasswordEntry(t *testing.T) { p := widget.NewPasswordEntry() p.Text = "visible" - r := test.WidgetRenderer(p) + r := test.TempWidgetRenderer(t, p) cont := r.Objects()[2].(*container.Scroll).Content.(fyne.Widget) - r = test.WidgetRenderer(cont) + r = test.TempWidgetRenderer(t, cont) rich := r.Objects()[1].(*widget.RichText) - r = test.WidgetRenderer(rich) + r = test.TempWidgetRenderer(t, rich) assert.Equal(t, "•••••••", r.Objects()[0].(*canvas.Text).Text) } diff --git a/widget/entry_test.go b/widget/entry_test.go index 00072e52db..7cc0c9773b 100644 --- a/widget/entry_test.go +++ b/widget/entry_test.go @@ -222,6 +222,38 @@ func TestEntry_Control_Word(t *testing.T) { assert.Equal(t, "", entry.SelectedText()) } +func TestEntry_Control_DeleteWord(t *testing.T) { + entry := widget.NewMultiLineEntry() + entry.SetText("Hello world\nhere is a second line") + entry.CursorRow = 1 + entry.CursorColumn = 10 // right before "second" + modifier := fyne.KeyModifierControl + if runtime.GOOS == "darwin" { + modifier = fyne.KeyModifierAlt + } + // Ctrl+delete - delete word to right ("second") + entry.TypedShortcut(&desktop.CustomShortcut{Modifier: modifier, KeyName: fyne.KeyDelete}) + assert.Equal(t, "Hello world\nhere is a line", entry.Text) + assert.Equal(t, 10, entry.CursorColumn) + + entry.CursorColumn = 8 // right before "a" + // Ctrl+backspace - delete word to left ("is") + entry.TypedShortcut(&desktop.CustomShortcut{Modifier: modifier, KeyName: fyne.KeyBackspace}) + assert.Equal(t, "Hello world\nhere a line", entry.Text) + assert.Equal(t, 5, entry.CursorColumn) + + // does nothing when nothing left to delete + entry.SetText("") + entry.TypedShortcut(&desktop.CustomShortcut{Modifier: modifier, KeyName: fyne.KeyBackspace}) + assert.Equal(t, "", entry.Text) + + // doesn't crash when trying to delete backward with one space + entry.SetText(" ") + entry.CursorRow = 0 + entry.CursorColumn = 1 + entry.TypedShortcut(&desktop.CustomShortcut{Modifier: modifier, KeyName: fyne.KeyBackspace}) +} + func TestEntry_CursorColumn_Wrap(t *testing.T) { entry := widget.NewMultiLineEntry() entry.SetText("a\nb") @@ -1818,7 +1850,7 @@ func TestPasswordEntry_ActionItemSizeAndPlacement(t *testing.T) { b := widget.NewButton("", func() {}) b.Icon = theme.CancelIcon() e.ActionItem = b - test.WidgetRenderer(e).Layout(e.MinSize()) + 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()) } @@ -1883,34 +1915,34 @@ func TestPasswordEntry_Reveal(t *testing.T) { test.AssertRendersToMarkup(t, "password_entry/initial.xml", c) c.Focus(entry) - test.Type(entry, "Hié™שרה") - assert.Equal(t, "Hié™שרה", entry.Text) + test.Type(entry, "Secret") + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/concealed.xml", c) // update the Password field entry.Password = false entry.Refresh() - assert.Equal(t, "Hié™שרה", entry.Text) + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/revealed.xml", c) assert.Equal(t, entry, c.Focused()) // update the Password field entry.Password = true entry.Refresh() - assert.Equal(t, "Hié™שרה", entry.Text) + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/concealed.xml", c) assert.Equal(t, entry, c.Focused()) // tap on action icon tapPos := fyne.NewPos(140-theme.InnerPadding()-theme.IconInlineSize()/2, 10+entry.Size().Height/2) test.TapCanvas(c, tapPos) - assert.Equal(t, "Hié™שרה", entry.Text) + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/revealed.xml", c) assert.Equal(t, entry, c.Focused()) // tap on action icon test.TapCanvas(c, tapPos) - assert.Equal(t, "Hié™שרה", entry.Text) + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/concealed.xml", c) assert.Equal(t, entry, c.Focused()) }) @@ -1931,14 +1963,14 @@ func TestPasswordEntry_Reveal(t *testing.T) { test.AssertRendersToMarkup(t, "password_entry/initial.xml", c) c.Focus(entry) - test.Type(entry, "Hié™שרה") - assert.Equal(t, "Hié™שרה", entry.Text) + test.Type(entry, "Secret") + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/concealed.xml", c) // update the Password field entry.Password = false entry.Refresh() - assert.Equal(t, "Hié™שרה", entry.Text) + assert.Equal(t, "Secret", entry.Text) test.AssertRendersToMarkup(t, "password_entry/revealed.xml", c) assert.Equal(t, entry, c.Focused()) }) diff --git a/widget/fileicon.go b/widget/fileicon.go index 0d885b3e6c..ec93f693ef 100644 --- a/widget/fileicon.go +++ b/widget/fileicon.go @@ -43,6 +43,7 @@ func (i *FileIcon) SetURI(uri fyne.URI) { i.Refresh() } +// must be called with i.propertyLock RLocked func (i *FileIcon) setURI(uri fyne.URI) { if uri == nil { i.resource = i.themeWithLock().Icon(theme.IconNameFile) @@ -96,6 +97,7 @@ func (i *FileIcon) SetSelected(selected bool) { i.Refresh() } +// must be called with i.propertyLock RLocked func (i *FileIcon) lookupIcon(uri fyne.URI) fyne.Resource { if icon, ok := uri.(fyne.URIWithIcon); ok { return icon.Icon() diff --git a/widget/form.go b/widget/form.go index ac4349f280..2aa43ed9ac 100644 --- a/widget/form.go +++ b/widget/form.go @@ -51,6 +51,12 @@ type Form struct { SubmitText string CancelText string + // Orientation allows a form to be vertical (a single column), horizontal (default, label then input) + // or to adapt according to the orientation of the mobile device (adaptive). + // + // Since: 2.5 + Orientation Orientation + itemGrid *fyne.Container buttonBox *fyne.Container cancelButton *Button @@ -95,6 +101,13 @@ func (f *Form) Refresh() { f.ensureRenderItems() f.updateButtons() f.updateLabels() + + if f.isVertical() { + f.itemGrid.Layout = layout.NewVBoxLayout() + } else { + f.itemGrid.Layout = layout.NewFormLayout() + } + f.BaseWidget.Refresh() canvas.Refresh(f.super()) // refresh ourselves for BG color - the above updates the content } @@ -179,14 +192,19 @@ func (f *Form) itemWidgetHasValidator(w fyne.CanvasObject) bool { return validator != nil } -func (f *Form) createLabel(text string) *canvas.Text { +func (f *Form) createLabel(text string) fyne.CanvasObject { th := f.Theme() v := fyne.CurrentApp().Settings().ThemeVariant() - return &canvas.Text{Text: text, + label := &canvas.Text{Text: text, Alignment: fyne.TextAlignTrailing, Color: th.Color(theme.ColorNameForeground, v), TextSize: th.Size(theme.SizeNameText), TextStyle: fyne.TextStyle{Bold: true}} + if f.isVertical() { + label.Alignment = fyne.TextAlignLeading + } + + return &fyne.Container{Layout: &formLabelLayout{form: f}, Objects: []fyne.CanvasObject{label}} } func (f *Form) updateButtons() { @@ -261,6 +279,22 @@ func (f *Form) ensureRenderItems() { f.itemGrid.Objects = append(f.itemGrid.Objects, objects...) } +func (f *Form) isVertical() bool { + if f.Orientation == Vertical { + return true + } else if f.Orientation == Horizontal { + return false + } + + dev := fyne.CurrentDevice() + if dev.IsMobile() { + orient := dev.Orientation() + return orient == fyne.OrientationVertical || orient == fyne.OrientationVerticalUpsideDown + } + + return false +} + func (f *Form) setUpValidation(widget fyne.CanvasObject, i int) { updateValidation := func(err error) { if err == errFormItemInitialState { @@ -335,7 +369,7 @@ func (f *Form) updateLabels() { v := fyne.CurrentApp().Settings().ThemeVariant() for i, item := range f.Items { - l := f.itemGrid.Objects[i*2].(*canvas.Text) + l := f.itemGrid.Objects[i*2].(*fyne.Container).Objects[0].(*canvas.Text) l.TextSize = th.Size(theme.SizeNameText) if dis, ok := item.Widget.(fyne.Disableable); ok { if dis.Disabled() { @@ -348,6 +382,11 @@ func (f *Form) updateLabels() { } l.Text = item.Text + if f.isVertical() { + l.Alignment = fyne.TextAlignLeading + } else { + l.Alignment = fyne.TextAlignTrailing + } l.Refresh() f.updateHelperText(item) } @@ -364,6 +403,11 @@ func (f *Form) CreateRenderer() fyne.WidgetRenderer { f.validationError = errFormItemInitialState // set initial state error to guarantee next error (if triggers) is always different f.itemGrid = &fyne.Container{Layout: layout.NewFormLayout()} + if f.isVertical() { + f.itemGrid.Layout = layout.NewVBoxLayout() + } else { + f.itemGrid.Layout = layout.NewFormLayout() + } content := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: []fyne.CanvasObject{f.itemGrid, f.buttonBox}} renderer := NewSimpleRenderer(content) f.ensureRenderItems() @@ -382,6 +426,33 @@ func NewForm(items ...*FormItem) *Form { return form } +type formLabelLayout struct { + form *Form +} + +func (f formLabelLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + xPad := innerPad + yPos := float32(0) + if !f.form.isVertical() { + xPad += innerPad + yPos = innerPad + } + objs[0].Move(fyne.NewPos(innerPad, yPos)) + objs[0].Resize(fyne.NewSize(size.Width-xPad, objs[0].MinSize().Height)) +} + +func (f formLabelLayout) MinSize(objs []fyne.CanvasObject) fyne.Size { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + min0 := objs[0].MinSize() + + if !f.form.isVertical() { + min0 = min0.AddWidthHeight(innerPad, 0) + } + + return min0.AddWidthHeight(innerPad, 0) +} + type formItemLayout struct { form *Form } diff --git a/widget/form_extend_test.go b/widget/form_extend_test.go index b5bef19930..7cc8a53cdb 100644 --- a/widget/form_extend_test.go +++ b/widget/form_extend_test.go @@ -16,7 +16,7 @@ func TestForm_Extended_CreateRenderer(t *testing.T) { form := &extendedForm{} form.ExtendBaseWidget(form) form.Items = []*FormItem{{Text: "test1", Widget: NewEntry()}} - assert.NotNil(t, test.WidgetRenderer(form)) + assert.NotNil(t, test.TempWidgetRenderer(t, form)) assert.Equal(t, 2, len(form.itemGrid.Objects)) form.Append("test2", NewEntry()) diff --git a/widget/form_test.go b/widget/form_test.go index d8aaac88ee..08088431e1 100644 --- a/widget/form_test.go +++ b/widget/form_test.go @@ -25,7 +25,7 @@ func TestFormSize(t *testing.T) { func TestForm_CreateRenderer(t *testing.T) { form := &Form{Items: []*FormItem{{Text: "test1", Widget: NewEntry()}}} - assert.NotNil(t, test.WidgetRenderer(form)) + assert.NotNil(t, test.TempWidgetRenderer(t, form)) assert.Equal(t, 2, len(form.itemGrid.Objects)) form.Append("test2", NewEntry()) @@ -48,14 +48,14 @@ func TestForm_Append(t *testing.T) { func TestForm_Append_Items(t *testing.T) { form := &Form{Items: []*FormItem{{Text: "test1", Widget: NewEntry()}}} assert.Equal(t, 1, len(form.Items)) - renderer := test.WidgetRenderer(form) + renderer := test.TempWidgetRenderer(t, form) form.Items = append(form.Items, NewFormItem("test2", NewEntry())) assert.True(t, len(form.Items) == 2) form.Refresh() c := renderer.Objects()[0].(*fyne.Container).Objects[0].(*fyne.Container) - assert.Equal(t, "test2", c.Objects[2].(*canvas.Text).Text) + assert.Equal(t, "test2", c.Objects[2].(*fyne.Container).Objects[0].(*canvas.Text).Text) } func TestForm_CustomButtonsText(t *testing.T) { @@ -116,13 +116,13 @@ func TestForm_ChangeText(t *testing.T) { item := NewFormItem("Test", NewEntry()) form := NewForm(item) - renderer := test.WidgetRenderer(form) + renderer := test.TempWidgetRenderer(t, form) c := renderer.Objects()[0].(*fyne.Container).Objects[0].(*fyne.Container) - assert.Equal(t, "Test", c.Objects[0].(*canvas.Text).Text) + assert.Equal(t, "Test", c.Objects[0].(*fyne.Container).Objects[0].(*canvas.Text).Text) item.Text = "Changed" form.Refresh() - assert.Equal(t, "Changed", c.Objects[0].(*canvas.Text).Text) + assert.Equal(t, "Changed", c.Objects[0].(*fyne.Container).Objects[0].(*canvas.Text).Text) } func TestForm_ChangeTheme(t *testing.T) { diff --git a/widget/hyperlink.go b/widget/hyperlink.go index 899eb10b0b..ae1cb7bac6 100644 --- a/widget/hyperlink.go +++ b/widget/hyperlink.go @@ -23,11 +23,17 @@ type Hyperlink struct { Alignment fyne.TextAlign // The alignment of the Text Wrapping fyne.TextWrap // The wrapping of the Text TextStyle fyne.TextStyle // The style of the hyperlink text + // The truncation mode of the hyperlink // // Since: 2.5 Truncation fyne.TextTruncation + // The theme size name for the text size of the hyperlink + // + // Since: 2.5 + SizeName fyne.ThemeSizeName + // OnTapped overrides the default `fyne.OpenURL` call when the link is tapped // // Since: 2.2 @@ -275,15 +281,19 @@ func (hl *Hyperlink) syncSegments() { Text: hl.Text, }, } - return + } else { + segment := hl.provider.Segments[0].(*TextSegment) + segment.Style.Alignment = hl.Alignment + segment.Style.TextStyle = hl.TextStyle + segment.Text = hl.Text } - segment := hl.provider.Segments[0].(*TextSegment) - segment.Style.Alignment = hl.Alignment - segment.Style.TextStyle = hl.TextStyle - segment.Text = hl.Text - - hl.textSize = fyne.MeasureText(hl.Text, th.Size(theme.SizeNameText), hl.TextStyle) + sizeName := hl.SizeName + if sizeName == "" { + sizeName = theme.SizeNameText + } + hl.provider.Segments[0].(*TextSegment).Style.SizeName = sizeName + hl.textSize = fyne.MeasureText(hl.Text, th.Size(sizeName), hl.TextStyle) } var _ fyne.WidgetRenderer = (*hyperlinkRenderer)(nil) diff --git a/widget/hyperlink_test.go b/widget/hyperlink_test.go index 9fd083ad4a..33104f1495 100644 --- a/widget/hyperlink_test.go +++ b/widget/hyperlink_test.go @@ -167,7 +167,7 @@ func TestHyperlink_SetUrl(t *testing.T) { func TestHyperlink_ThemeOverride(t *testing.T) { test.NewTempApp(t) - test.ApplyTheme(t, internalTest.LightTheme(theme.DefaultTheme())) + test.ApplyTheme(t, test.Theme()) hyperlink := &Hyperlink{Text: "Test"} bg := canvas.NewRectangle(color.Gray{Y: 0xc0}) @@ -178,12 +178,12 @@ func TestHyperlink_ThemeOverride(t *testing.T) { w.Resize(hyperlink.MinSize()) light := w.Canvas().Capture() - test.ApplyTheme(t, test.Theme()) + test.ApplyTheme(t, test.NewTheme()) hyperlink.Refresh() ugly := w.Canvas().Capture() assertPixelsMatch(t, false, ugly, light) - cache.OverrideTheme(hyperlink, internalTest.LightTheme(theme.DefaultTheme())) + cache.OverrideTheme(hyperlink, test.Theme()) hyperlink.Refresh() override := w.Canvas().Capture() assertPixelsMatch(t, true, override, light) diff --git a/widget/icon.go b/widget/icon.go index 695d42a2d5..bd30e413ab 100644 --- a/widget/icon.go +++ b/widget/icon.go @@ -3,7 +3,6 @@ package widget import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/theme" ) @@ -32,12 +31,6 @@ func (i *iconRenderer) Refresh() { return } - r := i.image.Resource - if r != nil { - r = cache.OverrideResourceTheme(i.image.Resource, i.image) - i.image.Resource = r - } - i.image.propertyLock.RLock() i.raster.Resource = i.image.Resource i.image.cachedRes = i.image.Resource @@ -76,12 +69,7 @@ func (i *Icon) CreateRenderer() fyne.WidgetRenderer { i.propertyLock.RLock() defer i.propertyLock.RUnlock() - res := i.Resource - if res != nil { - res = cache.OverrideResourceTheme(i.Resource, i) - i.Resource = res - } - img := canvas.NewImageFromResource(res) + img := canvas.NewImageFromResource(i.Resource) img.FillMode = canvas.ImageFillContain r := &iconRenderer{image: i, raster: img} diff --git a/widget/icon_extend_test.go b/widget/icon_extend_test.go index f404b4efdc..e4c86c51c6 100644 --- a/widget/icon_extend_test.go +++ b/widget/icon_extend_test.go @@ -25,5 +25,5 @@ func TestIcon_Extended_SetResource(t *testing.T) { objs := cache.Renderer(icon).Objects() assert.Equal(t, 1, len(objs)) - assert.Equal(t, theme.ComputerIcon(), objs[0].(*canvas.Image).Resource.(*cache.WidgetResource).ThemedResource) + assert.Equal(t, theme.ComputerIcon(), objs[0].(*canvas.Image).Resource) } diff --git a/widget/icon_internal_test.go b/widget/icon_internal_test.go index 4ce819b80f..3156870d9b 100644 --- a/widget/icon_internal_test.go +++ b/widget/icon_internal_test.go @@ -4,7 +4,6 @@ import ( "testing" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" "github.com/stretchr/testify/assert" @@ -12,7 +11,7 @@ import ( func TestNewIcon(t *testing.T) { icon := NewIcon(theme.ConfirmIcon()) - render := test.WidgetRenderer(icon) + render := test.TempWidgetRenderer(t, icon) assert.Equal(t, 1, len(render.Objects())) obj := render.Objects()[0] @@ -20,12 +19,12 @@ func TestNewIcon(t *testing.T) { if !ok { t.Fail() } - assert.Equal(t, theme.ConfirmIcon(), img.Resource.(*cache.WidgetResource).ThemedResource) + assert.Equal(t, theme.ConfirmIcon(), img.Resource) } func TestIcon_Nil(t *testing.T) { icon := NewIcon(nil) - render := test.WidgetRenderer(icon) + render := test.TempWidgetRenderer(t, icon) assert.Equal(t, 1, len(render.Objects())) assert.Nil(t, render.Objects()[0].(*canvas.Image).Resource) @@ -41,7 +40,7 @@ func TestIcon_MinSize(t *testing.T) { func TestIconRenderer_ApplyTheme(t *testing.T) { icon := NewIcon(theme.CancelIcon()) - render := test.WidgetRenderer(icon).(*iconRenderer) + render := test.TempWidgetRenderer(t, icon).(*iconRenderer) visible := render.Objects()[0].Visible() render.Refresh() diff --git a/widget/label_test.go b/widget/label_test.go index e09e110605..aca03304a2 100644 --- a/widget/label_test.go +++ b/widget/label_test.go @@ -128,14 +128,14 @@ func TestText_MinSize_MultiLine(t *testing.T) { textOneLine := NewLabel("Break") min := textOneLine.MinSize() textMultiLine := NewLabel("Bre\nak") - rich := test.WidgetRenderer(textMultiLine).Objects()[0].(*RichText) + rich := test.TempWidgetRenderer(t, textMultiLine).Objects()[0].(*RichText) min2 := textMultiLine.MinSize() assert.True(t, min2.Width < min.Width) assert.True(t, min2.Height > min.Height) yPos := float32(-1) - for _, text := range test.WidgetRenderer(rich).(*textRenderer).Objects() { + for _, text := range test.TempWidgetRenderer(t, rich).(*textRenderer).Objects() { assert.True(t, text.Size().Height < min2.Height) assert.True(t, text.Position().Y > yPos) yPos = text.Position().Y @@ -158,9 +158,9 @@ func TestText_MinSizeAdjustsWithContent(t *testing.T) { func TestLabel_ApplyTheme(t *testing.T) { text := NewLabel("Line 1") text.Hide() - rich := test.WidgetRenderer(text).Objects()[0].(*RichText) + rich := test.TempWidgetRenderer(t, text).Objects()[0].(*RichText) - render := test.WidgetRenderer(rich).(*textRenderer) + render := test.TempWidgetRenderer(t, rich).(*textRenderer) assert.Equal(t, theme.ForegroundColor(), render.Objects()[0].(*canvas.Text).Color) text.Show() assert.Equal(t, theme.ForegroundColor(), render.Objects()[0].(*canvas.Text).Color) diff --git a/widget/list.go b/widget/list.go index 88f1eecdce..dffa53dc1a 100644 --- a/widget/list.go +++ b/widget/list.go @@ -36,6 +36,11 @@ type List struct { OnSelected func(id ListItemID) `json:"-"` OnUnselected func(id ListItemID) `json:"-"` + // HideSeparators hides the separators between list rows + // + // Since: 2.5 + HideSeparators bool + currentFocus ListItemID focused bool scroller *widget.Scroll @@ -365,15 +370,15 @@ func (l *List) contentMinSize() fyne.Size { } height := float32(0) + totalCustom := 0 templateHeight := l.itemMin.Height - for item := 0; item < items; item++ { - itemHeight, ok := l.itemHeights[item] - if ok { + for id, itemHeight := range l.itemHeights { + if id < items { + totalCustom++ height += itemHeight - } else { - height += templateHeight } } + height += float32(items-totalCustom) * templateHeight return fyne.NewSize(l.itemMin.Width, height+separatorThickness*float32(items-1)) } @@ -772,6 +777,10 @@ func (l *listLayout) updateList(newOnly bool) { } func (l *listLayout) updateSeparators() { + if l.list.HideSeparators { + l.separators = nil + return + } if lenChildren := len(l.children); lenChildren > 1 { if lenSep := len(l.separators); lenSep > lenChildren { l.separators = l.separators[:lenChildren] diff --git a/widget/list_test.go b/widget/list_test.go index 2d339dbf09..64751ad080 100644 --- a/widget/list_test.go +++ b/widget/list_test.go @@ -28,7 +28,7 @@ func TestNewList(t *testing.T) { assert.Equal(t, 1000, list.Length()) assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) - assert.Equal(t, list.MinSize(), template.MinSize().Max(test.WidgetRenderer(list).(*listRenderer).scroller.MinSize())) + assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) assert.Equal(t, float32(0), list.offsetY) } @@ -51,7 +51,7 @@ func TestNewListWithData(t *testing.T) { assert.Equal(t, 1000, list.Length()) assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) - assert.Equal(t, list.MinSize(), template.MinSize().Max(test.WidgetRenderer(list).(*listRenderer).scroller.MinSize())) + assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) assert.Equal(t, float32(0), list.offsetY) } @@ -118,7 +118,7 @@ func TestList_SetItemHeight(t *testing.T) { func(ListItemID, fyne.CanvasObject) { }) - lay := test.WidgetRenderer(list).(*listRenderer).layout + lay := test.TempWidgetRenderer(t, list).(*listRenderer).layout assert.Equal(t, fyne.NewSize(32, 32), list.MinSize()) assert.Equal(t, fyne.NewSize(10, 10*5+(4*theme.Padding())), lay.MinSize()) @@ -165,7 +165,7 @@ func TestList_OffsetChange(t *testing.T) { assert.Equal(t, float32(0), list.offsetY) - scroll := test.WidgetRenderer(list).(*listRenderer).scroller + scroll := test.TempWidgetRenderer(t, list).(*listRenderer).scroller scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -280)}) assert.NotEqual(t, 0, list.offsetY) @@ -665,3 +665,29 @@ func TestList_RefreshUpdatesAllItems(t *testing.T) { list.Refresh() assert.Equal(t, "0.0.", printOut) } + +var minSize fyne.Size + +func BenchmarkContentMinSize(b *testing.B) { + b.StopTimer() + + l := NewList( + func() int { return 1000000 }, + func() fyne.CanvasObject { + return NewLabel("Test") + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*Label).SetText(fmt.Sprintf("%d", id)) + }, + ) + l.SetItemHeight(10, 55) + l.SetItemHeight(12345, 2) + + min := fyne.Size{} + b.StartTimer() + for i := 0; i < b.N; i++ { + min = l.contentMinSize() + } + + minSize = min +} diff --git a/widget/markdown.go b/widget/markdown.go index 8bbf8e41d2..50ffbbb174 100644 --- a/widget/markdown.go +++ b/widget/markdown.go @@ -27,6 +27,18 @@ func (t *RichText) ParseMarkdown(content string) { t.Refresh() } +// AppendMarkdown parses the given markdown string and appends the +// content to the widget, with the appropriate formatting. +// This API is intended for appending complete markdown documents or +// standalone fragments, and should not be used to parse a single +// markdown document piecewise. +// +// Since: 2.5 +func (t *RichText) AppendMarkdown(content string) { + t.Segments = append(t.Segments, parseMarkdown(content)...) + t.Refresh() +} + type markdownRenderer []RichTextSegment func (m *markdownRenderer) AddOptions(...renderer.Option) {} @@ -107,7 +119,7 @@ func renderNode(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, // These empty text elements indicate single line breaks after non-text elements in goldmark. return []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: " "}}, nil } - text = suffixSpaceIfAppropriate(text, source, n) + text = suffixSpaceIfAppropriate(text, n) if blockquote { return []RichTextSegment{&TextSegment{Style: RichTextStyleBlockquote, Text: text}}, nil } @@ -125,7 +137,7 @@ func renderNode(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, return nil, nil } -func suffixSpaceIfAppropriate(text string, source []byte, n ast.Node) string { +func suffixSpaceIfAppropriate(text string, n ast.Node) string { next := n.NextSibling() if next != nil && next.Type() == ast.TypeInline && !strings.HasSuffix(text, " ") { return text + " " diff --git a/widget/menu.go b/widget/menu.go index c1acb6fa38..f48d743df9 100644 --- a/widget/menu.go +++ b/widget/menu.go @@ -241,9 +241,8 @@ func (r *menuRenderer) Layout(s fyne.Size) { scrollSize := boxSize if c := fyne.CurrentApp().Driver().CanvasForObject(r.m.super()); c != nil { ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m.super()) - pos, size := c.InteractiveArea() - bottomPad := c.Size().Height - pos.Y - size.Height - if ah := c.Size().Height - bottomPad - ap.Y; ah < boxSize.Height { + _, areaSize := c.InteractiveArea() + if ah := areaSize.Height - ap.Y; ah < boxSize.Height { scrollSize = fyne.NewSize(boxSize.Width, ah) } } diff --git a/widget/menu_desktop_test.go b/widget/menu_desktop_test.go index abecb879fc..f8138b332e 100644 --- a/widget/menu_desktop_test.go +++ b/widget/menu_desktop_test.go @@ -106,8 +106,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(500, 300), menuPos: fyne.NewPos(10, 10), mousePositions: []fyne.Position{ - fyne.NewPos(30, 100), - fyne.NewPos(100, 170), + fyne.NewPos(32, 103), + fyne.NewPos(102, 173), }, want: "menu/desktop/layout_normal_with_submenus.xml", }, @@ -126,8 +126,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(500, 300), menuPos: fyne.NewPos(410, 10), mousePositions: []fyne.Position{ - fyne.NewPos(430, 100), // open submenu - fyne.NewPos(300, 170), // open subsubmenu + fyne.NewPos(432, 103), // open submenu + fyne.NewPos(302, 173), // open subsubmenu }, want: "menu/desktop/layout_no_space_on_right.xml", }, @@ -135,8 +135,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(200, 300), menuPos: fyne.NewPos(10, 10), mousePositions: []fyne.Position{ - fyne.NewPos(30, 100), // open submenu - fyne.NewPos(100, 170), // open subsubmenu + fyne.NewPos(32, 103), // open submenu + fyne.NewPos(102, 173), // open subsubmenu }, want: "menu/desktop/layout_no_space_on_both_sides.xml", }, @@ -165,7 +165,7 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(300, 800), menuPos: fyne.NewPos(10, 10), mousePositions: []fyne.Position{ - fyne.NewPos(30, 140), // open submenu + fyne.NewPos(32, 143), // open submenu }, want: shortcutsMasterPrefixPath + ".xml", wantImage: shortcutsMasterPrefixPath + ".png", @@ -173,6 +173,9 @@ func TestMenu_Layout(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { + if name == "menu with shortcuts" && (runtime.GOOS == "windows" || runtime.GOOS == "darwin") { + return // macOS and Windows are off-by-a-fraction for symbol font TODO find out why + } w.Resize(tt.windowSize) m := widget.NewMenu(menu) o := internalWidget.NewOverlayContainer(m, c, nil) @@ -213,7 +216,9 @@ func TestMenu_Scrolling(t *testing.T) { fyne.NewMenuItem("F", nil), ) - w.Resize(fyne.NewSize(100, 100)) + // 100x100 + // + 4,5 for canvas’ safe area + w.Resize(fyne.NewSize(104, 105)) m := widget.NewMenu(menu) o := internalWidget.NewOverlayContainer(m, c, nil) c.Overlays().Add(o) @@ -257,7 +262,8 @@ func TestMenu_TraverseMenu(t *testing.T) { fyne.NewMenuItem("Baz", nil), )) w.SetContent(internalWidget.NewOverlayContainer(m, c, nil)) - w.Resize(m.MinSize()) + // + 4,5 for canvas’ safe area + w.Resize(m.MinSize().AddWidthHeight(4, 5)) m.Resize(m.MinSize()) // going all the way down … diff --git a/widget/menu_mobile_test.go b/widget/menu_mobile_test.go index cbaa8f91c6..19a249e257 100644 --- a/widget/menu_mobile_test.go +++ b/widget/menu_mobile_test.go @@ -56,8 +56,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(500, 300), menuPos: fyne.NewPos(10, 10), tapPositions: []fyne.Position{ - fyne.NewPos(30, 100), - fyne.NewPos(100, 170), + fyne.NewPos(32, 103), + fyne.NewPos(102, 173), }, want: "menu/mobile/layout_normal_with_submenus.xml", }, @@ -76,8 +76,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(500, 300), menuPos: fyne.NewPos(410, 10), tapPositions: []fyne.Position{ - fyne.NewPos(430, 100), // open submenu - fyne.NewPos(300, 170), // open subsubmenu + fyne.NewPos(432, 103), // open submenu + fyne.NewPos(302, 173), // open subsubmenu }, want: "menu/mobile/layout_no_space_on_right.xml", }, @@ -85,8 +85,8 @@ func TestMenu_Layout(t *testing.T) { windowSize: fyne.NewSize(200, 300), menuPos: fyne.NewPos(10, 10), tapPositions: []fyne.Position{ - fyne.NewPos(30, 100), // open submenu - fyne.NewPos(100, 170), // open subsubmenu + fyne.NewPos(32, 103), // open submenu + fyne.NewPos(102, 173), // open subsubmenu }, want: "menu/mobile/layout_no_space_on_both_sides.xml", }, @@ -150,7 +150,9 @@ func TestMenu_Dragging(t *testing.T) { fyne.NewMenuItem("F", nil), ) - w.Resize(fyne.NewSize(100, 100)) + // 100x100 + // + 4,5 for canvas’ safe area + w.Resize(fyne.NewSize(104, 105)) m := widget.NewMenu(menu) o := internalWidget.NewOverlayContainer(m, c, nil) c.Overlays().Add(o) diff --git a/widget/menu_test.go b/widget/menu_test.go index 13bf6bcab7..87cf7213dc 100644 --- a/widget/menu_test.go +++ b/widget/menu_test.go @@ -34,7 +34,8 @@ func TestMenu_RefreshOptions(t *testing.T) { itemBaz, )) w.SetContent(internalWidget.NewOverlayContainer(m, c, nil)) - w.Resize(m.MinSize()) + // + 4,5 for canvas’ safe area + w.Resize(m.MinSize().AddWidthHeight(4, 5)) m.Resize(m.MinSize()) test.AssertRendersToMarkup(t, "menu/refresh_initial.xml", c) diff --git a/widget/popup_menu.go b/widget/popup_menu.go index 8cf2ecf96b..6b5c3cd9e4 100644 --- a/widget/popup_menu.go +++ b/widget/popup_menu.go @@ -140,14 +140,15 @@ func (p *PopUpMenu) TypedRune(rune) {} func (p *PopUpMenu) adjustedPosition(pos fyne.Position, size fyne.Size) fyne.Position { x := pos.X y := pos.Y - if x+size.Width > p.canvas.Size().Width { - x = p.canvas.Size().Width - size.Width + _, areaSize := p.canvas.InteractiveArea() + if x+size.Width > areaSize.Width { + x = areaSize.Width - size.Width if x < 0 { x = 0 // TODO here we may need a scroller as it's wider than our canvas } } - if y+size.Height > p.canvas.Size().Height { - y = p.canvas.Size().Height - size.Height + if y+size.Height > areaSize.Height { + y = areaSize.Height - size.Height if y < 0 { y = 0 // TODO here we may need a scroller as it's longer than our canvas } diff --git a/widget/popup_menu_test.go b/widget/popup_menu_test.go index bd1bec1dc5..36514f1b18 100644 --- a/widget/popup_menu_test.go +++ b/widget/popup_menu_test.go @@ -44,7 +44,8 @@ func TestPopUpMenu_Resize(t *testing.T) { largeSize := c.Size().Add(fyne.NewSize(10, 10)) m.Resize(largeSize) test.AssertRendersToMarkup(t, "popup_menu/canvas_too_small.xml", c) - assert.Equal(t, fyne.NewSize(largeSize.Width, c.Size().Height), m.Size(), "width is larger than canvas; height is limited by canvas (menu scrolls)") + _, areaSize := c.InteractiveArea() + assert.Equal(t, fyne.NewSize(largeSize.Width, areaSize.Height), m.Size(), "width is larger than canvas; height is limited by canvas (menu scrolls)") } func TestPopUpMenu_Show(t *testing.T) { @@ -82,12 +83,13 @@ func TestPopUpMenu_ShowAtPosition(t *testing.T) { m.Hide() test.AssertRendersToMarkup(t, "popup_menu/hidden.xml", c) - menuSize := c.Size().Add(fyne.NewSize(10, 10)) - m.Resize(menuSize) m.ShowAtPosition(fyne.NewPos(10, 10)) + menuSize := c.Size().Add(fyne.NewSize(10, 10)) + m.Resize(menuSize) test.AssertRendersToMarkup(t, "popup_menu/canvas_too_small.xml", c) - assert.Equal(t, fyne.NewSize(menuSize.Width, c.Size().Height), m.Size(), "width is larger than canvas; height is limited by canvas (menu scrolls)") + _, areaSize := c.InteractiveArea() + assert.Equal(t, fyne.NewSize(menuSize.Width, areaSize.Height), m.Size(), "width is larger than canvas; height is limited by canvas (menu scrolls)") } func setupPopUpMenuTest() (*widget.PopUpMenu, fyne.Window) { diff --git a/widget/popup_test.go b/widget/popup_test.go index a7277c28c5..020ef5466e 100644 --- a/widget/popup_test.go +++ b/widget/popup_test.go @@ -77,7 +77,8 @@ func TestShowPopUpAtRelativePosition(t *testing.T) { if assert.NotNil(t, pop) { assert.True(t, pop.Visible()) assert.Equal(t, 1, len(w.Canvas().Overlays().List())) - assert.Equal(t, pos.Add(parent2.Position()).Add(fyne.NewPos(theme.Padding()*2, theme.Padding()*2)), pop.(*PopUp).Content.Position()) + areaPos, _ := w.Canvas().InteractiveArea() + assert.Equal(t, pos.Add(parent2.Position()).Add(fyne.NewPos(theme.Padding()*2, theme.Padding()*2)).Subtract(areaPos), pop.(*PopUp).Content.Position()) } } diff --git a/widget/progressbar.go b/widget/progressbar.go index cfae197db9..9a37f9053b 100644 --- a/widget/progressbar.go +++ b/widget/progressbar.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/data/binding" - "fyne.io/fyne/v2/internal/cache" col "fyne.io/fyne/v2/internal/color" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/theme" @@ -17,6 +16,7 @@ type progressRenderer struct { widget.BaseRenderer background, bar canvas.Rectangle label canvas.Text + ratio float32 progress *ProgressBar } @@ -25,18 +25,17 @@ type progressRenderer struct { func (p *progressRenderer) MinSize() fyne.Size { th := p.progress.Theme() - var tsize fyne.Size - if text := p.progress.TextFormatter; text != nil { - tsize = fyne.MeasureText(text(), p.label.TextSize, p.label.TextStyle) - } else { - tsize = fyne.MeasureText("100%", p.label.TextSize, p.label.TextStyle) + text := "100%" + if format := p.progress.TextFormatter; format != nil { + text = format() } padding := th.Size(theme.SizeNameInnerPadding) * 2 - return fyne.NewSize(tsize.Width+padding, tsize.Height+padding) + size := fyne.MeasureText(text, p.label.TextSize, p.label.TextStyle) + return size.AddWidthHeight(padding, padding) } -func (p *progressRenderer) updateBar() { +func (p *progressRenderer) layoutBar(size fyne.Size) { if p.progress.Value < p.progress.Min { p.progress.Value = p.progress.Min } @@ -44,24 +43,30 @@ func (p *progressRenderer) updateBar() { p.progress.Value = p.progress.Max } - delta := float32(p.progress.Max - p.progress.Min) - ratio := float32(p.progress.Value-p.progress.Min) / delta + delta := p.progress.Max - p.progress.Min + p.ratio = float32((p.progress.Value - p.progress.Min) / delta) + p.bar.Resize(fyne.NewSize(size.Width*p.ratio, size.Height)) +} + +func (p *progressRenderer) updateBar() { + p.layoutBar(p.progress.Size()) + + // Don't draw rectangles when they can't be seen. + p.background.Hidden = p.ratio == 1.0 + p.bar.Hidden = p.ratio == 0.0 if text := p.progress.TextFormatter; text != nil { p.label.Text = text() } else { - p.label.Text = strconv.Itoa(int(ratio*100)) + "%" + p.label.Text = strconv.Itoa(int(p.ratio*100)) + "%" } - - size := p.progress.Size() - p.bar.Resize(fyne.NewSize(size.Width*ratio, size.Height)) } // Layout the components of the check widget func (p *progressRenderer) Layout(size fyne.Size) { p.background.Resize(size) p.label.Resize(size) - p.updateBar() + p.layoutBar(size) } // applyTheme updates the progress bar to match the current theme @@ -171,10 +176,9 @@ func (p *ProgressBar) Unbind() { // The default Min is 0 and Max is 1, Values set should be between those numbers. // The display will convert this to a percentage. func NewProgressBar() *ProgressBar { - p := &ProgressBar{Min: 0, Max: 1} - - cache.Renderer(p).Layout(p.MinSize()) - return p + bar := &ProgressBar{Min: 0, Max: 1} + bar.ExtendBaseWidget(bar) + return bar } // NewProgressBarWithData returns a progress bar connected with the specified data source. @@ -183,7 +187,6 @@ func NewProgressBar() *ProgressBar { func NewProgressBarWithData(data binding.Float) *ProgressBar { p := NewProgressBar() p.Bind(data) - return p } diff --git a/widget/progressbar_extend_test.go b/widget/progressbar_extend_test.go index d483ef01ac..01487b8c50 100644 --- a/widget/progressbar_extend_test.go +++ b/widget/progressbar_extend_test.go @@ -22,7 +22,7 @@ func newExtendedProgressBar() *extendedProgressBar { func TestProgressBarRenderer_Extended_Layout(t *testing.T) { bar := newExtendedProgressBar() bar.Resize(fyne.NewSize(100, 100)) - r := test.WidgetRenderer(bar).(*progressRenderer) + r := test.TempWidgetRenderer(t, bar).(*progressRenderer) assert.Equal(t, 0.0, bar.Value) assert.Equal(t, float32(0), r.bar.Size().Width) diff --git a/widget/progressbar_test.go b/widget/progressbar_test.go index aeac1603ee..2095349815 100644 --- a/widget/progressbar_test.go +++ b/widget/progressbar_test.go @@ -12,7 +12,7 @@ import ( var globalProgressRenderer fyne.WidgetRenderer -func BenchmarkProgressbar(b *testing.B) { +func BenchmarkProgressbarCreateRenderer(b *testing.B) { var renderer fyne.WidgetRenderer widget := &ProgressBar{} b.ReportAllocs() @@ -25,6 +25,17 @@ func BenchmarkProgressbar(b *testing.B) { globalProgressRenderer = renderer } +func BenchmarkProgressBarLayout(b *testing.B) { + b.ReportAllocs() // We should see zero allocations. + + bar := &ProgressBar{} + renderer := bar.CreateRenderer() + + for i := 0; i < b.N; i++ { + renderer.Layout(fyne.NewSize(100, 100)) + } +} + func TestNewProgressBarWithData(t *testing.T) { val := binding.NewFloat() val.Set(0.4) @@ -86,7 +97,7 @@ func TestProgressRenderer_Layout(t *testing.T) { bar := NewProgressBar() bar.Resize(fyne.NewSize(100, 10)) - render := test.WidgetRenderer(bar).(*progressRenderer) + render := test.TempWidgetRenderer(t, bar).(*progressRenderer) assert.Equal(t, float32(0), render.bar.Size().Width) bar.SetValue(.5) @@ -100,7 +111,7 @@ func TestProgressRenderer_Layout_Overflow(t *testing.T) { bar := NewProgressBar() bar.Resize(fyne.NewSize(100, 10)) - render := test.WidgetRenderer(bar).(*progressRenderer) + render := test.TempWidgetRenderer(t, bar).(*progressRenderer) bar.SetValue(1) assert.Equal(t, bar.Size().Width, render.bar.Size().Width) @@ -110,7 +121,7 @@ func TestProgressRenderer_Layout_Overflow(t *testing.T) { func TestProgressRenderer_ApplyTheme(t *testing.T) { bar := NewProgressBar() - render := test.WidgetRenderer(bar).(*progressRenderer) + render := test.TempWidgetRenderer(t, bar).(*progressRenderer) oldLabelColor := render.label.Color render.Refresh() diff --git a/widget/progressbarinfinite.go b/widget/progressbarinfinite.go index 86fb38d066..c7d161a634 100644 --- a/widget/progressbarinfinite.go +++ b/widget/progressbarinfinite.go @@ -5,7 +5,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/theme" ) @@ -21,7 +20,7 @@ type infProgressRenderer struct { widget.BaseRenderer background, bar canvas.Rectangle animation fyne.Animation - running bool + wasRunning bool progress *ProgressBarInfinite } @@ -64,8 +63,15 @@ func (p *infProgressRenderer) Layout(size fyne.Size) { // Refresh updates the size and position of the horizontal scrolling infinite progress bar func (p *infProgressRenderer) Refresh() { - if p.isRunning() { + running := p.progress.Running() + if running { + if !p.wasRunning { + p.start() + } return // we refresh from the goroutine + } else if p.wasRunning { + p.stop() + return } th := p.progress.Theme() @@ -82,27 +88,17 @@ func (p *infProgressRenderer) Refresh() { canvas.Refresh(p.progress.super()) } -func (p *infProgressRenderer) isRunning() bool { - p.progress.propertyLock.RLock() - defer p.progress.propertyLock.RUnlock() - - return p.running -} - // Start the infinite progress bar background thread to update it continuously func (p *infProgressRenderer) start() { - if p.isRunning() { - return - } - p.progress.propertyLock.Lock() defer p.progress.propertyLock.Unlock() + p.animation.Duration = time.Second * 3 p.animation.Tick = p.updateBar p.animation.Curve = fyne.AnimationLinear p.animation.RepeatCount = fyne.AnimationRepeatForever - p.running = true + p.wasRunning = true p.animation.Start() } @@ -111,11 +107,15 @@ func (p *infProgressRenderer) stop() { p.progress.propertyLock.Lock() defer p.progress.propertyLock.Unlock() - p.running = false + p.wasRunning = false p.animation.Stop() } func (p *infProgressRenderer) Destroy() { + p.progress.propertyLock.Lock() + p.progress.running = false + p.progress.propertyLock.Unlock() + p.stop() } @@ -123,37 +123,60 @@ func (p *infProgressRenderer) Destroy() { // An infinite progress bar loops 0% -> 100% repeatedly until Stop() is called type ProgressBarInfinite struct { BaseWidget + running bool } // Show this widget, if it was previously hidden func (p *ProgressBarInfinite) Show() { - p.Start() + p.propertyLock.Lock() + p.running = true + p.propertyLock.Unlock() + p.BaseWidget.Show() } // Hide this widget, if it was previously visible func (p *ProgressBarInfinite) Hide() { - p.Stop() + p.propertyLock.Lock() + p.running = false + p.propertyLock.Unlock() + p.BaseWidget.Hide() } // Start the infinite progress bar animation func (p *ProgressBarInfinite) Start() { - cache.Renderer(p).(*infProgressRenderer).start() + p.propertyLock.Lock() + if p.running { + p.propertyLock.Unlock() + return + } + + p.running = true + p.propertyLock.Unlock() + + p.BaseWidget.Refresh() } // Stop the infinite progress bar animation func (p *ProgressBarInfinite) Stop() { - cache.Renderer(p).(*infProgressRenderer).stop() + p.propertyLock.Lock() + if !p.running { + p.propertyLock.Unlock() + return + } + + p.running = false + p.propertyLock.Unlock() + + p.BaseWidget.Refresh() } // Running returns the current state of the infinite progress animation func (p *ProgressBarInfinite) Running() bool { - if !cache.IsRendered(p) { - return false - } - - return cache.Renderer(p).(*infProgressRenderer).isRunning() + p.propertyLock.RLock() + defer p.propertyLock.RUnlock() + return p.running } // MinSize returns the size that this widget should not shrink below @@ -185,7 +208,10 @@ func (p *ProgressBarInfinite) CreateRenderer() fyne.WidgetRenderer { render.SetObjects([]fyne.CanvasObject{&render.background, &render.bar}) - render.start() + p.propertyLock.Lock() + p.running = true + p.propertyLock.Unlock() + return render } @@ -193,7 +219,7 @@ func (p *ProgressBarInfinite) CreateRenderer() fyne.WidgetRenderer { // SetValue() is not defined for infinite progress bar // To stop the looping progress and set the progress bar to 100%, call ProgressBarInfinite.Stop() func NewProgressBarInfinite() *ProgressBarInfinite { - p := &ProgressBarInfinite{} - cache.Renderer(p).Layout(p.MinSize()) - return p + bar := &ProgressBarInfinite{} + bar.ExtendBaseWidget(bar) + return bar } diff --git a/widget/progressbarinfinite_test.go b/widget/progressbarinfinite_test.go index fe0d50797d..42fbb1b75d 100644 --- a/widget/progressbarinfinite_test.go +++ b/widget/progressbarinfinite_test.go @@ -27,12 +27,24 @@ func BenchmarkProgressbarInf(b *testing.B) { func TestProgressBarInfinite_Creation(t *testing.T) { bar := NewProgressBarInfinite() - // ticker should start automatically + + // ticker should be disabled until the widget is shown + assert.False(t, bar.Running()) + + win := test.NewWindow(bar) + defer win.Close() + win.Show() + + // ticker should start automatically once the renderer is created assert.True(t, bar.Running()) } func TestProgressBarInfinite_Destroy(t *testing.T) { bar := NewProgressBarInfinite() + win := test.NewWindow(bar) + defer win.Close() + win.Show() + assert.True(t, cache.IsRendered(bar)) assert.True(t, bar.Running()) @@ -46,6 +58,9 @@ func TestProgressBarInfinite_Destroy(t *testing.T) { func TestProgressBarInfinite_Reshown(t *testing.T) { bar := NewProgressBarInfinite() + win := test.NewWindow(bar) + defer win.Close() + win.Show() assert.True(t, bar.Running()) bar.Hide() @@ -65,7 +80,7 @@ func TestInfiniteProgressRenderer_Layout(t *testing.T) { width := float32(100.0) bar.Resize(fyne.NewSize(width, 10)) - render := test.WidgetRenderer(bar).(*infProgressRenderer) + render := test.TempWidgetRenderer(t, bar).(*infProgressRenderer) render.updateBar(0.0) // start at the smallest size diff --git a/widget/radio_group_extended_test.go b/widget/radio_group_extended_test.go index 7e71898320..0e6a788597 100644 --- a/widget/radio_group_extended_test.go +++ b/widget/radio_group_extended_test.go @@ -130,9 +130,9 @@ func TestRadioGroup_Extended_Hovered(t *testing.T) { t.Run(tt.name, func(t *testing.T) { radio := newextendedRadioGroup(tt.options, nil) radio.Horizontal = tt.isHorizontal - item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + item1 := test.TempWidgetRenderer(t, radio).Objects()[0].(*radioItem) render1 := cache.Renderer(item1).(*radioItemRenderer) - render2 := cache.Renderer(test.WidgetRenderer(radio).Objects()[1].(*radioItem)).(*radioItemRenderer) + render2 := cache.Renderer(test.TempWidgetRenderer(t, radio).Objects()[1].(*radioItem)).(*radioItemRenderer) assert.False(t, item1.hovered) assert.Equal(t, color.Transparent, render1.focusIndicator.FillColor) @@ -165,7 +165,7 @@ func TestRadioGroup_Extended_Hovered(t *testing.T) { func TestRadioGroupRenderer_Extended_ApplyTheme(t *testing.T) { radio := newextendedRadioGroup([]string{"Test"}, func(string) {}) - render := cache.Renderer(test.WidgetRenderer(radio).Objects()[0].(*radioItem)).(*radioItemRenderer) + render := cache.Renderer(test.TempWidgetRenderer(t, radio).Objects()[0].(*radioItem)).(*radioItemRenderer) textSize := render.label.TextSize customTextSize := textSize @@ -179,7 +179,7 @@ func TestRadioGroupRenderer_Extended_ApplyTheme(t *testing.T) { func extendedRadioGroupTestTapItem(t *testing.T, radio *extendedRadioGroup, item int) { t.Helper() - renderer := test.WidgetRenderer(radio) + renderer := test.TempWidgetRenderer(t, radio) radioItem := renderer.Objects()[item].(*radioItem) radioItem.Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) } diff --git a/widget/radio_group_internal_test.go b/widget/radio_group_internal_test.go index 3a292969ff..940833a818 100644 --- a/widget/radio_group_internal_test.go +++ b/widget/radio_group_internal_test.go @@ -95,26 +95,26 @@ func TestRadioGroup_Append(t *testing.T) { radio := NewRadioGroup([]string{"Hi"}, nil) assert.Equal(t, 1, len(radio.Options)) - assert.Equal(t, 1, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 1, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) radio.Options = append(radio.Options, "Another") radio.Refresh() assert.Equal(t, 2, len(radio.Options)) - assert.Equal(t, 2, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 2, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) } func TestRadioGroup_Remove(t *testing.T) { radio := NewRadioGroup([]string{"Hi", "Another"}, nil) assert.Equal(t, 2, len(radio.Options)) - assert.Equal(t, 2, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 2, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) radio.Options = radio.Options[:1] radio.Refresh() assert.Equal(t, 1, len(radio.Options)) - assert.Equal(t, 1, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 1, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) } func TestRadioGroup_SetSelected(t *testing.T) { @@ -156,14 +156,14 @@ func TestRadioGroup_DuplicatedOptions(t *testing.T) { radio := NewRadioGroup([]string{"Hi", "Hi", "Hi", "Another", "Another"}, nil) assert.Equal(t, 5, len(radio.Options)) - assert.Equal(t, 5, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 5, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) radioGroupTestTapItem(t, radio, 1) assert.Equal(t, "Hi", radio.Selected) assert.Equal(t, 1, radio.selectedIndex()) - item0 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + item0 := test.TempWidgetRenderer(t, radio).Objects()[0].(*radioItem) assert.Equal(t, false, item0.focused) - item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + item1 := test.TempWidgetRenderer(t, radio).Objects()[0].(*radioItem) assert.Equal(t, false, item1.focused) } @@ -173,7 +173,7 @@ func TestRadioGroup_AppendDuplicate(t *testing.T) { radio.Append("Hi") assert.Equal(t, 2, len(radio.Options)) - assert.Equal(t, 2, len(test.WidgetRenderer(radio).(*radioGroupRenderer).items)) + assert.Equal(t, 2, len(test.TempWidgetRenderer(t, radio).(*radioGroupRenderer).items)) } func TestRadioGroup_Disable(t *testing.T) { @@ -235,7 +235,7 @@ func TestRadioGroup_Hovered(t *testing.T) { t.Run(tt.name, func(t *testing.T) { radio := NewRadioGroup(tt.options, nil) radio.Horizontal = tt.isHorizontal - item1 := test.WidgetRenderer(radio).Objects()[0].(*radioItem) + item1 := test.TempWidgetRenderer(t, radio).Objects()[0].(*radioItem) render1 := radioGroupTestItemRenderer(t, radio, 0) render2 := radioGroupTestItemRenderer(t, radio, 1) @@ -311,11 +311,12 @@ func TestRadioGroupRenderer_ApplyTheme(t *testing.T) { func radioGroupTestTapItem(t *testing.T, radio *RadioGroup, item int) { t.Helper() - renderer := test.WidgetRenderer(radio) + renderer := test.TempWidgetRenderer(t, radio) radioItem := renderer.Objects()[item].(*radioItem) radioItem.Tapped(&fyne.PointEvent{Position: fyne.NewPos(theme.Padding(), theme.Padding())}) } func radioGroupTestItemRenderer(t *testing.T, radio *RadioGroup, item int) *radioItemRenderer { - return cache.Renderer(test.WidgetRenderer(radio).Objects()[item].(fyne.Widget)).(*radioItemRenderer) + t.Cleanup(func() { cache.DestroyRenderer(radio) }) + return cache.Renderer(test.TempWidgetRenderer(t, radio).Objects()[item].(fyne.Widget)).(*radioItemRenderer) } diff --git a/widget/radio_item_test.go b/widget/radio_item_test.go index 0f63906e14..2331ce38a0 100644 --- a/widget/radio_item_test.go +++ b/widget/radio_item_test.go @@ -27,7 +27,7 @@ func BenchmarkRadioCreateRenderer(b *testing.B) { func TestRadioItem_FocusIndicator_Centered_Vertically(t *testing.T) { item := newRadioItem("Hello", nil) - render := test.WidgetRenderer(item).(*radioItemRenderer) + render := test.TempWidgetRenderer(t, item).(*radioItemRenderer) render.Layout(fyne.NewSize(200, 100)) focusIndicatorSize := theme.IconInlineSize() + 2*theme.Padding() diff --git a/widget/richtext.go b/widget/richtext.go index d9474253a2..73af8ce5ba 100644 --- a/widget/richtext.go +++ b/widget/richtext.go @@ -787,7 +787,7 @@ func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign for i, text := range texts { var size fyne.Size if txt, ok := text.(*canvas.Text); ok { - s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle) + s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle, txt.FontSource) if base > tallestBaseline { if tallestBaseline > 0 { realign = true @@ -799,7 +799,7 @@ func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign } else if c, ok := text.(*fyne.Container); ok { wid := c.Objects[0] if link, ok := wid.(*Hyperlink); ok { - s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle) + s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle, nil) if base > tallestBaseline { if tallestBaseline > 0 { realign = true @@ -1110,7 +1110,7 @@ func splitLines(seg *TextSegment) []rowBoundary { } func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int, bool) { - face := paint.CachedFontFace(text.TextStyle, text.TextSize, 1.0) + face := paint.CachedFontFace(text.TextStyle, text.FontSource, text) runes := []rune(s) in := shaping.Input{ @@ -1118,7 +1118,7 @@ func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int RunStart: 0, RunEnd: len(ellipsis), Direction: di.DirectionLTR, - Face: face.Fonts[0], + Face: face.Fonts.ResolveFace('…'), Size: float32ToFixed266(text.TextSize), } shaper := &shaping.HarfbuzzShaper{} diff --git a/widget/richtext_objects_test.go b/widget/richtext_objects_test.go index ff22b923b5..ff55c93699 100644 --- a/widget/richtext_objects_test.go +++ b/widget/richtext_objects_test.go @@ -15,7 +15,7 @@ import ( func TestRichText_Image(t *testing.T) { img := &ImageSegment{Title: "test", Source: storage.NewFileURI("./testdata/richtext/richtext_multiline.png")} text := NewRichText(img) - texts := test.WidgetRenderer(text).Objects() + texts := test.TempWidgetRenderer(t, text).Objects() drawn := texts[0].(*richImage).img text.Resize(fyne.NewSize(200, 200)) @@ -37,10 +37,10 @@ func TestRichText_HyperLink(t *testing.T) { &TextSegment{Text: "Text"}, &HyperlinkSegment{Text: "Link"}, }}) - texts := test.WidgetRenderer(text).Objects() + texts := test.TempWidgetRenderer(t, text).Objects() assert.Equal(t, "Text", texts[0].(*canvas.Text).Text) - richLink := test.WidgetRenderer(texts[1].(*fyne.Container).Objects[0].(*Hyperlink)).Objects()[0].(fyne.Widget) - linkText := test.WidgetRenderer(richLink).Objects()[0].(*canvas.Text) + richLink := test.TempWidgetRenderer(t, texts[1].(*fyne.Container).Objects[0].(*Hyperlink)).Objects()[0].(fyne.Widget) + linkText := test.TempWidgetRenderer(t, richLink).Objects()[0].(*canvas.Text) assert.Equal(t, "Link", linkText.Text) c := test.NewCanvas() @@ -54,7 +54,7 @@ func TestRichText_List(t *testing.T) { text := NewRichText(&ListSegment{Items: []RichTextSegment{ seg, }}) - texts := test.WidgetRenderer(text).Objects() + texts := test.TempWidgetRenderer(t, text).Objects() assert.Equal(t, "•", strings.TrimSpace(texts[0].(*canvas.Text).Text)) assert.Equal(t, "Test", texts[1].(*canvas.Text).Text) } @@ -64,7 +64,7 @@ func TestRichText_OrderedList(t *testing.T) { &TextSegment{Text: "One"}, &TextSegment{Text: "Two"}, }}) - texts := test.WidgetRenderer(text).Objects() + texts := test.TempWidgetRenderer(t, text).Objects() assert.Equal(t, "1.", strings.TrimSpace(texts[0].(*canvas.Text).Text)) assert.Equal(t, "One", texts[1].(*canvas.Text).Text) assert.Equal(t, "2.", strings.TrimSpace(texts[2].(*canvas.Text).Text)) diff --git a/widget/richtext_test.go b/widget/richtext_test.go index eaff83bd0b..11379a0a2b 100644 --- a/widget/richtext_test.go +++ b/widget/richtext_test.go @@ -49,7 +49,7 @@ func TestText_Alignment(t *testing.T) { seg := trailingBoldErrorSegment() seg.Text = "Test" text := NewRichText(seg) - assert.Equal(t, fyne.TextAlignTrailing, test.WidgetRenderer(text).Objects()[0].(*canvas.Text).Alignment) + assert.Equal(t, fyne.TextAlignTrailing, test.TempWidgetRenderer(t, text).Objects()[0].(*canvas.Text).Alignment) } func TestText_Row(t *testing.T) { @@ -136,8 +136,8 @@ func TestText_Scroll(t *testing.T) { assert.Less(t, text4.MinSize().Width, text3.MinSize().Width) assert.Equal(t, text4.MinSize().Height, text3.MinSize().Height) - content3 := test.WidgetRenderer(text3).Objects()[0].(*widget.Scroll).Content - content4 := test.WidgetRenderer(text4).Objects()[0].(*widget.Scroll).Content + content3 := test.TempWidgetRenderer(t, text3).Objects()[0].(*widget.Scroll).Content + content4 := test.TempWidgetRenderer(t, text4).Objects()[0].(*widget.Scroll).Content assert.Less(t, content4.MinSize().Width, content3.MinSize().Width) assert.Greater(t, content4.MinSize().Height, content3.MinSize().Height) } diff --git a/widget/select_entry_internal_test.go b/widget/select_entry_internal_test.go index fac8c3e3db..25efc2952e 100644 --- a/widget/select_entry_internal_test.go +++ b/widget/select_entry_internal_test.go @@ -29,7 +29,8 @@ func TestSelectEntry_Disableable(t *testing.T) { test.TapCanvas(c, switchPos) test.AssertRendersToMarkup(t, "select_entry/disableable_enabled_opened.xml", c) - test.TapCanvas(c, fyne.NewPos(0, 0)) + areaPos, _ := c.InteractiveArea() + test.TapCanvas(c, areaPos) test.AssertRendersToMarkup(t, "select_entry/disableable_enabled_tapped_selected.xml", c) e.Disable() @@ -87,6 +88,7 @@ func TestSelectEntry_DropDownMove(t *testing.T) { entrySize := e.MinSize() w.Resize(entrySize.Add(fyne.NewSize(100, 100))) e.Resize(entrySize) + inset, _ := w.Canvas().InteractiveArea() // open the popup test.Tap(e.ActionItem.(fyne.Tappable)) @@ -95,7 +97,7 @@ func TestSelectEntry_DropDownMove(t *testing.T) { e.Move(fyne.NewPos(10, 10)) assert.Equal(t, fyne.NewPos(10, 10), e.Entry.Position()) assert.Equal(t, - fyne.NewPos(10, 10+entrySize.Height-theme.InputBorderSize()), + fyne.NewPos(10, 10+entrySize.Height-theme.InputBorderSize()).Subtract(inset), e.popUp.Position(), ) @@ -103,7 +105,7 @@ func TestSelectEntry_DropDownMove(t *testing.T) { e.Move(fyne.NewPos(30, 27)) assert.Equal(t, fyne.NewPos(30, 27), e.Entry.Position()) assert.Equal(t, - fyne.NewPos(30, 27+entrySize.Height-theme.InputBorderSize()), + fyne.NewPos(30, 27+entrySize.Height-theme.InputBorderSize()).Subtract(inset), e.popUp.Position(), ) } diff --git a/widget/select_internal_test.go b/widget/select_internal_test.go index 1a63fb687a..afa753d4f5 100644 --- a/widget/select_internal_test.go +++ b/widget/select_internal_test.go @@ -51,7 +51,7 @@ func TestSelectRenderer_TapAnimation(t *testing.T) { path = "select/mobile/tap_animation.png" } - render1 := test.WidgetRenderer(sel).(*selectRenderer) + render1 := test.TempWidgetRenderer(t, sel).(*selectRenderer) test.Tap(sel) sel.popUp.Hide() sel.tapAnim.Tick(0.5) @@ -60,7 +60,7 @@ func TestSelectRenderer_TapAnimation(t *testing.T) { cache.DestroyRenderer(sel) sel.Refresh() - render2 := test.WidgetRenderer(sel).(*selectRenderer) + render2 := test.TempWidgetRenderer(t, sel).(*selectRenderer) assert.NotEqual(t, render1, render2) diff --git a/widget/select_test.go b/widget/select_test.go index 2410cc717a..cd4fa75409 100644 --- a/widget/select_test.go +++ b/widget/select_test.go @@ -196,21 +196,22 @@ func TestSelect_KeyboardControl(t *testing.T) { w.Resize(fyne.NewSize(150, 200)) c := w.Canvas() c.Focus(sel) + areaPos, _ := c.InteractiveArea() test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected.xml", c) sel.TypedKey(&fyne.KeyEvent{Name: fyne.KeySpace}) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected_popup.xml", c) - test.TapCanvas(c, fyne.NewPos(0, 0)) + test.TapCanvas(c, areaPos) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected.xml", c) sel.TypedKey(&fyne.KeyEvent{Name: fyne.KeyUp}) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected_popup.xml", c) - test.TapCanvas(c, fyne.NewPos(0, 0)) + test.TapCanvas(c, areaPos) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected.xml", c) sel.TypedKey(&fyne.KeyEvent{Name: fyne.KeyDown}) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected_popup.xml", c) - test.TapCanvas(c, fyne.NewPos(0, 0)) + test.TapCanvas(c, areaPos) test.AssertRendersToMarkup(t, "select/kbdctrl_none_selected.xml", c) sel.TypedKey(&fyne.KeyEvent{Name: fyne.KeyEnter}) diff --git a/widget/slider.go b/widget/slider.go index 6601139684..781a5c08a2 100644 --- a/widget/slider.go +++ b/widget/slider.go @@ -13,15 +13,6 @@ import ( "fyne.io/fyne/v2/theme" ) -// Orientation controls the horizontal/vertical layout of a widget -type Orientation int - -// Orientation constants to control widget layout -const ( - Horizontal Orientation = 0 - Vertical Orientation = 1 -) - var _ fyne.Draggable = (*Slider)(nil) var _ fyne.Focusable = (*Slider)(nil) var _ desktop.Hoverable = (*Slider)(nil) diff --git a/widget/slider_test.go b/widget/slider_test.go index 9f504d577f..421e153dac 100644 --- a/widget/slider_test.go +++ b/widget/slider_test.go @@ -74,7 +74,7 @@ func TestSlider_HorizontalLayout(t *testing.T) { slider := NewSlider(0, 1) slider.Resize(fyne.NewSize(100, 10)) - render := test.WidgetRenderer(slider).(*sliderRenderer) + render := test.TempWidgetRenderer(t, slider).(*sliderRenderer) wSize := render.slider.Size() tSize := render.track.Size() aSize := render.active.Size() @@ -116,7 +116,7 @@ func TestSlider_VerticalLayout(t *testing.T) { slider.Orientation = Vertical slider.Resize(fyne.NewSize(10, 100)) - render := test.WidgetRenderer(slider).(*sliderRenderer) + render := test.TempWidgetRenderer(t, slider).(*sliderRenderer) wSize := render.slider.Size() tSize := render.track.Size() aSize := render.active.Size() diff --git a/widget/table.go b/widget/table.go index 444341bc91..6a4d341092 100644 --- a/widget/table.go +++ b/widget/table.go @@ -82,6 +82,11 @@ type Table struct { // Since: 2.4 StickyColumnCount int + // HideSeparators hides the separator lines between the table cells + // + // Since: 2.5 + HideSeparators bool + currentFocus TableCellID focused bool selectedCell, hoveredCell *TableCellID @@ -1043,6 +1048,11 @@ func (t *tableRenderer) Layout(s fyne.Size) { t.t.corner.Resize(fyne.NewSize(off.X, off.Y)) t.t.dividerLayer.Resize(s) + if t.t.HideSeparators { + t.t.dividerLayer.Hide() + } else { + t.t.dividerLayer.Show() + } } func (t *tableRenderer) MinSize() fyne.Size { diff --git a/widget/table_test.go b/widget/table_test.go index 07066067e5..649efa1576 100644 --- a/widget/table_test.go +++ b/widget/table_test.go @@ -20,7 +20,7 @@ func TestTable_Empty(t *testing.T) { table.Resize(fyne.NewSize(120, 120)) table.CreateRenderer() - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) cellRenderer.Refresh() // let's not crash :) } @@ -39,7 +39,7 @@ func TestTable_Cache(t *testing.T) { c.SetPadded(false) c.Resize(fyne.NewSize(120, 148)) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) cellRenderer.Refresh() assert.Equal(t, 6, len(cellRenderer.(*tableCellsRenderer).visible)) assert.Equal(t, "Cell 0, 0", cellRenderer.Objects()[0].(*Label).Text) @@ -68,7 +68,7 @@ func TestTable_ChangeTheme(t *testing.T) { table.CreateRenderer() table.Resize(fyne.NewSize(50, 30)) - content := test.WidgetRenderer(table.content.Content.(*tableCells)).(*tableCellsRenderer) + content := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)).(*tableCellsRenderer) w := test.NewWindow(table) defer w.Close() w.Resize(fyne.NewSize(180, 180)) @@ -77,7 +77,7 @@ func TestTable_ChangeTheme(t *testing.T) { assert.Equal(t, NewLabel("placeholder").MinSize(), content.Objects()[0].(*Label).Size()) test.WithTestTheme(t, func() { - test.WidgetRenderer(table).Refresh() + test.TempWidgetRenderer(t, table).Refresh() test.AssertImageMatches(t, "table/theme_changed.png", w.Canvas().Capture()) }) assert.Equal(t, NewLabel("placeholder").MinSize(), content.Objects()[0].(*Label).Size()) @@ -155,7 +155,7 @@ func TestTable_Headers(t *testing.T) { }) table.Resize(fyne.NewSize(120, 120)) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) assert.Equal(t, "text", cellRenderer.(*tableCellsRenderer).Objects()[2].(*Label).Text) assert.Equal(t, "text", cellRenderer.(*tableCellsRenderer).Objects()[5].(*Label).Text) assert.True(t, areaContainsLabel(table.top.Content.(*fyne.Container).Objects, "A")) @@ -194,7 +194,7 @@ func TestTable_Sticky(t *testing.T) { }) table.Resize(fyne.NewSize(120, 120)) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)).(*tableCellsRenderer) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)).(*tableCellsRenderer) assert.True(t, areaContainsLabel(cellRenderer.Objects(), "text 0,0")) assert.True(t, areaContainsLabel(cellRenderer.Objects(), "text 1,0")) assert.True(t, areaContainsLabel(cellRenderer.Objects(), "text 2,1")) @@ -367,7 +367,7 @@ func TestTable_Refresh(t *testing.T) { }) table.Resize(fyne.NewSize(120, 120)) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) assert.Equal(t, "placeholder", cellRenderer.(*tableCellsRenderer).Objects()[7].(*Label).Text) displayText = "replaced" @@ -724,7 +724,7 @@ func TestTable_SetColumnWidth(t *testing.T) { table.Resize(fyne.NewSize(120, 120)) table.Select(TableCellID{1, 0}) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) cellRenderer.Refresh() assert.Equal(t, 8, len(cellRenderer.(*tableCellsRenderer).visible)) assert.Equal(t, float32(32), cellRenderer.(*tableCellsRenderer).Objects()[0].Size().Width) @@ -803,7 +803,7 @@ func TestTable_SetRowHeight(t *testing.T) { table.Resize(fyne.NewSize(120, 120)) table.Select(TableCellID{0, 1}) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) cellRenderer.Refresh() assert.Equal(t, 6, len(cellRenderer.(*tableCellsRenderer).visible)) assert.Equal(t, float32(48), cellRenderer.(*tableCellsRenderer).Objects()[0].Size().Height) @@ -870,7 +870,7 @@ func TestTable_ShowVisible(t *testing.T) { func(TableCellID, fyne.CanvasObject) {}) table.Resize(fyne.NewSize(120, 120)) - cellRenderer := test.WidgetRenderer(table.content.Content.(*tableCells)) + cellRenderer := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)) cellRenderer.Refresh() assert.Equal(t, 8, len(cellRenderer.(*tableCellsRenderer).visible)) } diff --git a/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items.xml b/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items.xml index 7569ce657b..ec1db5a0ee 100644 --- a/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items.xml +++ b/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items.xml @@ -19,7 +19,7 @@ - + diff --git a/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items_opened.xml b/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items_opened.xml index e5b898d0e3..4e54e150fc 100644 --- a/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items_opened.xml +++ b/widget/testdata/accordion/layout_expanded_multiple_open_multiple_items_opened.xml @@ -29,7 +29,7 @@ - + diff --git a/widget/testdata/accordion/layout_expanded_single_open_multiple_items.xml b/widget/testdata/accordion/layout_expanded_single_open_multiple_items.xml index 7569ce657b..ec1db5a0ee 100644 --- a/widget/testdata/accordion/layout_expanded_single_open_multiple_items.xml +++ b/widget/testdata/accordion/layout_expanded_single_open_multiple_items.xml @@ -19,7 +19,7 @@ - + diff --git a/widget/testdata/accordion/layout_expanded_single_open_multiple_items_opened.xml b/widget/testdata/accordion/layout_expanded_single_open_multiple_items_opened.xml index b20060cbaf..79c4d2346a 100644 --- a/widget/testdata/accordion/layout_expanded_single_open_multiple_items_opened.xml +++ b/widget/testdata/accordion/layout_expanded_single_open_multiple_items_opened.xml @@ -24,7 +24,7 @@ - + diff --git a/widget/testdata/accordion/layout_multiple_open_multiple_items.xml b/widget/testdata/accordion/layout_multiple_open_multiple_items.xml index 7d03a7d6c6..5a5ef468e7 100644 --- a/widget/testdata/accordion/layout_multiple_open_multiple_items.xml +++ b/widget/testdata/accordion/layout_multiple_open_multiple_items.xml @@ -19,7 +19,7 @@ - + diff --git a/widget/testdata/accordion/layout_multiple_open_multiple_items_opened.xml b/widget/testdata/accordion/layout_multiple_open_multiple_items_opened.xml index 9ba5a5dcdc..31620c9cc8 100644 --- a/widget/testdata/accordion/layout_multiple_open_multiple_items_opened.xml +++ b/widget/testdata/accordion/layout_multiple_open_multiple_items_opened.xml @@ -29,7 +29,7 @@ - + diff --git a/widget/testdata/accordion/layout_single_open_multiple_items.xml b/widget/testdata/accordion/layout_single_open_multiple_items.xml index 7d03a7d6c6..5a5ef468e7 100644 --- a/widget/testdata/accordion/layout_single_open_multiple_items.xml +++ b/widget/testdata/accordion/layout_single_open_multiple_items.xml @@ -19,7 +19,7 @@ - + diff --git a/widget/testdata/accordion/layout_single_open_multiple_items_opened.xml b/widget/testdata/accordion/layout_single_open_multiple_items_opened.xml index 6bd5d0cd0a..4e80548430 100644 --- a/widget/testdata/accordion/layout_single_open_multiple_items_opened.xml +++ b/widget/testdata/accordion/layout_single_open_multiple_items_opened.xml @@ -24,7 +24,7 @@ - + diff --git a/widget/testdata/accordion/theme_changed.png b/widget/testdata/accordion/theme_changed.png index a6f2a1166d..45817f9c74 100644 Binary files a/widget/testdata/accordion/theme_changed.png and b/widget/testdata/accordion/theme_changed.png differ diff --git a/widget/testdata/accordion/theme_initial.png b/widget/testdata/accordion/theme_initial.png index 58e09f7d09..dc69216916 100644 Binary files a/widget/testdata/accordion/theme_initial.png and b/widget/testdata/accordion/theme_initial.png differ diff --git a/widget/testdata/button/tap_animation.png b/widget/testdata/button/tap_animation.png index 32c5483e60..55cdffe814 100644 Binary files a/widget/testdata/button/tap_animation.png and b/widget/testdata/button/tap_animation.png differ diff --git a/widget/testdata/button/theme_changed.png b/widget/testdata/button/theme_changed.png index c76734079e..5a73f5d1c0 100644 Binary files a/widget/testdata/button/theme_changed.png and b/widget/testdata/button/theme_changed.png differ diff --git a/widget/testdata/card/layout_all_items.xml b/widget/testdata/card/layout_all_items.xml index d0c97f4543..3c6527ba5e 100644 --- a/widget/testdata/card/layout_all_items.xml +++ b/widget/testdata/card/layout_all_items.xml @@ -13,7 +13,7 @@ Longer title subtitle with length - + diff --git a/widget/testdata/card/layout_image_content.xml b/widget/testdata/card/layout_image_content.xml index cb0efb0047..947bb789d7 100644 --- a/widget/testdata/card/layout_image_content.xml +++ b/widget/testdata/card/layout_image_content.xml @@ -13,7 +13,7 @@ - + diff --git a/widget/testdata/card/layout_just_image.xml b/widget/testdata/card/layout_just_image.xml index 3e77789526..3c27bc168e 100644 --- a/widget/testdata/card/layout_just_image.xml +++ b/widget/testdata/card/layout_just_image.xml @@ -13,7 +13,7 @@ - + diff --git a/widget/testdata/card/layout_titles_image.xml b/widget/testdata/card/layout_titles_image.xml index 89271f4d99..96a7df5520 100644 --- a/widget/testdata/card/layout_titles_image.xml +++ b/widget/testdata/card/layout_titles_image.xml @@ -13,7 +13,7 @@ Title Subtitle - + diff --git a/widget/testdata/entry/focus_with_popup_initial.xml b/widget/testdata/entry/focus_with_popup_initial.xml index feb41a2621..3b16dfd27f 100644 --- a/widget/testdata/entry/focus_with_popup_initial.xml +++ b/widget/testdata/entry/focus_with_popup_initial.xml @@ -15,8 +15,8 @@ - - + + diff --git a/widget/testdata/entry/tab-content.png b/widget/testdata/entry/tab-content.png index eb124ac00e..f9e4410419 100644 Binary files a/widget/testdata/entry/tab-content.png and b/widget/testdata/entry/tab-content.png differ diff --git a/widget/testdata/entry/tab-select.png b/widget/testdata/entry/tab-select.png index 609d2c2a00..b41ed0f8f4 100644 Binary files a/widget/testdata/entry/tab-select.png and b/widget/testdata/entry/tab-select.png differ diff --git a/widget/testdata/entry/tapped_secondary_full_menu.xml b/widget/testdata/entry/tapped_secondary_full_menu.xml index 097366960b..13de8f5a59 100644 --- a/widget/testdata/entry/tapped_secondary_full_menu.xml +++ b/widget/testdata/entry/tapped_secondary_full_menu.xml @@ -15,8 +15,8 @@ - - + + diff --git a/widget/testdata/entry/tapped_secondary_password_menu.xml b/widget/testdata/entry/tapped_secondary_password_menu.xml index 1bbb8fafec..16435e3363 100644 --- a/widget/testdata/entry/tapped_secondary_password_menu.xml +++ b/widget/testdata/entry/tapped_secondary_password_menu.xml @@ -15,8 +15,8 @@ - - + + diff --git a/widget/testdata/entry/tapped_secondary_read_menu.xml b/widget/testdata/entry/tapped_secondary_read_menu.xml index a07b4217f6..bb13f17f41 100644 --- a/widget/testdata/entry/tapped_secondary_read_menu.xml +++ b/widget/testdata/entry/tapped_secondary_read_menu.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/entry/undo_redo_5undo.png b/widget/testdata/entry/undo_redo_5undo.png index a23615e1d0..a7ae273e1f 100644 Binary files a/widget/testdata/entry/undo_redo_5undo.png and b/widget/testdata/entry/undo_redo_5undo.png differ diff --git a/widget/testdata/entry/undo_redo_initial.png b/widget/testdata/entry/undo_redo_initial.png index 3055a600b9..a5fc70efc0 100644 Binary files a/widget/testdata/entry/undo_redo_initial.png and b/widget/testdata/entry/undo_redo_initial.png differ diff --git a/widget/testdata/entry/undo_redo_mistake_corrected.png b/widget/testdata/entry/undo_redo_mistake_corrected.png index 2afe9afeef..05bfe03764 100644 Binary files a/widget/testdata/entry/undo_redo_mistake_corrected.png and b/widget/testdata/entry/undo_redo_mistake_corrected.png differ diff --git a/widget/testdata/entry/undo_redo_mistkaes.png b/widget/testdata/entry/undo_redo_mistkaes.png index 930f5ab45f..aa83686805 100644 Binary files a/widget/testdata/entry/undo_redo_mistkaes.png and b/widget/testdata/entry/undo_redo_mistkaes.png differ diff --git a/widget/testdata/entry/validate_valid.xml b/widget/testdata/entry/validate_valid.xml index 8e89fa5a33..ecf591962d 100644 --- a/widget/testdata/entry/validate_valid.xml +++ b/widget/testdata/entry/validate_valid.xml @@ -10,7 +10,7 @@ - + diff --git a/widget/testdata/entry/wrap_multi_line_cursor.png b/widget/testdata/entry/wrap_multi_line_cursor.png index 1ad05b43d2..40470bdb5b 100644 Binary files a/widget/testdata/entry/wrap_multi_line_cursor.png and b/widget/testdata/entry/wrap_multi_line_cursor.png differ diff --git a/widget/testdata/form/extended_entry.xml b/widget/testdata/form/extended_entry.xml index d8b2214059..cd6ebedbad 100644 --- a/widget/testdata/form/extended_entry.xml +++ b/widget/testdata/form/extended_entry.xml @@ -3,7 +3,9 @@ - Extended entry + + Extended entry + diff --git a/widget/testdata/form/layout.xml b/widget/testdata/form/layout.xml index a2138037ae..de9444978a 100644 --- a/widget/testdata/form/layout.xml +++ b/widget/testdata/form/layout.xml @@ -3,7 +3,9 @@ - test1 + + test1 + @@ -18,7 +20,9 @@ - test2 + + test2 + diff --git a/widget/testdata/form/theme_changed.png b/widget/testdata/form/theme_changed.png index 53eb21a1d0..6af6640c05 100644 Binary files a/widget/testdata/form/theme_changed.png and b/widget/testdata/form/theme_changed.png differ diff --git a/widget/testdata/form/theme_initial.png b/widget/testdata/form/theme_initial.png index ea55abfc1e..b612617dca 100644 Binary files a/widget/testdata/form/theme_initial.png and b/widget/testdata/form/theme_initial.png differ diff --git a/widget/testdata/list/initial.xml b/widget/testdata/list/initial.xml index 46233cfec2..e43564c0fe 100644 --- a/widget/testdata/list/initial.xml +++ b/widget/testdata/list/initial.xml @@ -136,37 +136,37 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/widget/testdata/list/item_removed.xml b/widget/testdata/list/item_removed.xml index 0e190d3b05..68d265a365 100644 --- a/widget/testdata/list/item_removed.xml +++ b/widget/testdata/list/item_removed.xml @@ -28,10 +28,10 @@ - + - + diff --git a/widget/testdata/list/list_initial.png b/widget/testdata/list/list_initial.png index a62aeaa9d1..3750c3cf49 100644 Binary files a/widget/testdata/list/list_initial.png and b/widget/testdata/list/list_initial.png differ diff --git a/widget/testdata/list/list_item_height.png b/widget/testdata/list/list_item_height.png index 4ac807b0f8..fc45f0275f 100644 Binary files a/widget/testdata/list/list_item_height.png and b/widget/testdata/list/list_item_height.png differ diff --git a/widget/testdata/list/list_theme_changed.png b/widget/testdata/list/list_theme_changed.png index 378f2c2d6a..fc4cfc857d 100644 Binary files a/widget/testdata/list/list_theme_changed.png and b/widget/testdata/list/list_theme_changed.png differ diff --git a/widget/testdata/list/new_data.xml b/widget/testdata/list/new_data.xml index 56199d9682..fcc8c3f97c 100644 --- a/widget/testdata/list/new_data.xml +++ b/widget/testdata/list/new_data.xml @@ -136,37 +136,37 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/widget/testdata/list/offset_changed.xml b/widget/testdata/list/offset_changed.xml index 235ef5dd7b..73c8d7d507 100644 --- a/widget/testdata/list/offset_changed.xml +++ b/widget/testdata/list/offset_changed.xml @@ -136,37 +136,37 @@ - + - + - + - + - + - + - + - + - + - + - + diff --git a/widget/testdata/list/resized.xml b/widget/testdata/list/resized.xml index 18a9a89722..9b018ca69e 100644 --- a/widget/testdata/list/resized.xml +++ b/widget/testdata/list/resized.xml @@ -196,52 +196,52 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/widget/testdata/list/small.xml b/widget/testdata/list/small.xml index 0e190d3b05..68d265a365 100644 --- a/widget/testdata/list/small.xml +++ b/widget/testdata/list/small.xml @@ -28,10 +28,10 @@ - + - + diff --git a/widget/testdata/menu/desktop/layout_background_reset.xml b/widget/testdata/menu/desktop/layout_background_reset.xml index e9b782f250..6b8acac398 100644 --- a/widget/testdata/menu/desktop/layout_background_reset.xml +++ b/widget/testdata/menu/desktop/layout_background_reset.xml @@ -3,7 +3,7 @@ - + @@ -23,7 +23,7 @@ A - + @@ -31,14 +31,14 @@ C - + - + D - + diff --git a/widget/testdata/menu/desktop/layout_no_space_on_both_sides.xml b/widget/testdata/menu/desktop/layout_no_space_on_both_sides.xml index c17197aeec..21abcd5a40 100644 --- a/widget/testdata/menu/desktop/layout_no_space_on_both_sides.xml +++ b/widget/testdata/menu/desktop/layout_no_space_on_both_sides.xml @@ -3,7 +3,7 @@ - + @@ -23,7 +23,7 @@ A - + B (long) @@ -31,14 +31,14 @@ C - + - + D - + @@ -60,17 +60,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/desktop/layout_no_space_on_right.xml b/widget/testdata/menu/desktop/layout_no_space_on_right.xml index 67fa8dbe18..d1449ae378 100644 --- a/widget/testdata/menu/desktop/layout_no_space_on_right.xml +++ b/widget/testdata/menu/desktop/layout_no_space_on_right.xml @@ -3,7 +3,7 @@ - + @@ -23,7 +23,7 @@ A - + B (long) @@ -31,14 +31,14 @@ C - + - + D - + @@ -60,17 +60,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/desktop/layout_normal.png b/widget/testdata/menu/desktop/layout_normal.png index 8be2517dbe..16b8060eb2 100644 Binary files a/widget/testdata/menu/desktop/layout_normal.png and b/widget/testdata/menu/desktop/layout_normal.png differ diff --git a/widget/testdata/menu/desktop/layout_normal.xml b/widget/testdata/menu/desktop/layout_normal.xml index a945ed732b..8326518b26 100644 --- a/widget/testdata/menu/desktop/layout_normal.xml +++ b/widget/testdata/menu/desktop/layout_normal.xml @@ -3,7 +3,7 @@ - + @@ -23,21 +23,21 @@ A - + B (long) C - + - + D - + diff --git a/widget/testdata/menu/desktop/layout_normal_with_submenus.xml b/widget/testdata/menu/desktop/layout_normal_with_submenus.xml index b287e40628..0317428029 100644 --- a/widget/testdata/menu/desktop/layout_normal_with_submenus.xml +++ b/widget/testdata/menu/desktop/layout_normal_with_submenus.xml @@ -3,7 +3,7 @@ - + @@ -23,7 +23,7 @@ A - + B (long) @@ -31,14 +31,14 @@ C - + - + D - + @@ -60,17 +60,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/desktop/layout_shortcuts_darwin.png b/widget/testdata/menu/desktop/layout_shortcuts_darwin.png index fe90d477ff..82af4d1424 100644 Binary files a/widget/testdata/menu/desktop/layout_shortcuts_darwin.png and b/widget/testdata/menu/desktop/layout_shortcuts_darwin.png differ diff --git a/widget/testdata/menu/desktop/layout_shortcuts_darwin_theme_changed.png b/widget/testdata/menu/desktop/layout_shortcuts_darwin_theme_changed.png index cf092677cd..55f5493197 100644 Binary files a/widget/testdata/menu/desktop/layout_shortcuts_darwin_theme_changed.png and b/widget/testdata/menu/desktop/layout_shortcuts_darwin_theme_changed.png differ diff --git a/widget/testdata/menu/desktop/layout_shortcuts_other.png b/widget/testdata/menu/desktop/layout_shortcuts_other.png index 85fecec29c..a48ebd8ae8 100644 Binary files a/widget/testdata/menu/desktop/layout_shortcuts_other.png and b/widget/testdata/menu/desktop/layout_shortcuts_other.png differ diff --git a/widget/testdata/menu/desktop/layout_shortcuts_other.xml b/widget/testdata/menu/desktop/layout_shortcuts_other.xml index f1998ff28d..d65e8f0d26 100644 --- a/widget/testdata/menu/desktop/layout_shortcuts_other.xml +++ b/widget/testdata/menu/desktop/layout_shortcuts_other.xml @@ -3,7 +3,7 @@ - + @@ -23,22 +23,22 @@ A - + B (long) C - + - + D - + diff --git a/widget/testdata/menu/desktop/layout_shortcuts_other_theme_changed.png b/widget/testdata/menu/desktop/layout_shortcuts_other_theme_changed.png index f7bb2f4a4d..34e573d37d 100644 Binary files a/widget/testdata/menu/desktop/layout_shortcuts_other_theme_changed.png and b/widget/testdata/menu/desktop/layout_shortcuts_other_theme_changed.png differ diff --git a/widget/testdata/menu/desktop/layout_theme_changed.png b/widget/testdata/menu/desktop/layout_theme_changed.png index 6638a579cb..905a4137b0 100644 Binary files a/widget/testdata/menu/desktop/layout_theme_changed.png and b/widget/testdata/menu/desktop/layout_theme_changed.png differ diff --git a/widget/testdata/menu/desktop/layout_theme_changed.xml b/widget/testdata/menu/desktop/layout_theme_changed.xml index a945ed732b..8326518b26 100644 --- a/widget/testdata/menu/desktop/layout_theme_changed.xml +++ b/widget/testdata/menu/desktop/layout_theme_changed.xml @@ -3,7 +3,7 @@ - + @@ -23,21 +23,21 @@ A - + B (long) C - + - + D - + diff --git a/widget/testdata/menu/desktop/layout_window_too_short.xml b/widget/testdata/menu/desktop/layout_window_too_short.xml index f1deec7bdb..5b2b28a72f 100644 --- a/widget/testdata/menu/desktop/layout_window_too_short.xml +++ b/widget/testdata/menu/desktop/layout_window_too_short.xml @@ -3,19 +3,19 @@ - - - + + + - - - - - + + + + + - + @@ -23,28 +23,28 @@ A - + B (long) C - + - + D - + - + - + diff --git a/widget/testdata/menu/desktop/layout_window_too_short_for_submenu.xml b/widget/testdata/menu/desktop/layout_window_too_short_for_submenu.xml index 6543a4063e..d46f88f101 100644 --- a/widget/testdata/menu/desktop/layout_window_too_short_for_submenu.xml +++ b/widget/testdata/menu/desktop/layout_window_too_short_for_submenu.xml @@ -3,19 +3,19 @@ - - - + + + - - - - - + + + + + - + @@ -23,7 +23,7 @@ A - + B (long) @@ -31,23 +31,23 @@ C - + - + D - + - + - - - + + + @@ -68,18 +68,18 @@ subitem A - + subitem B - - + + subitem C (long) - - + + @@ -101,7 +101,7 @@ subsubitem A (long) - + subsubitem B diff --git a/widget/testdata/menu/desktop/scroll_bottom.xml b/widget/testdata/menu/desktop/scroll_bottom.xml index fef1fadb46..c8fbcd4043 100644 --- a/widget/testdata/menu/desktop/scroll_bottom.xml +++ b/widget/testdata/menu/desktop/scroll_bottom.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/desktop/scroll_middle.xml b/widget/testdata/menu/desktop/scroll_middle.xml index 5c0c69f979..b07e80d798 100644 --- a/widget/testdata/menu/desktop/scroll_middle.xml +++ b/widget/testdata/menu/desktop/scroll_middle.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/desktop/scroll_top.xml b/widget/testdata/menu/desktop/scroll_top.xml index a395d1c309..efe483f09d 100644 --- a/widget/testdata/menu/desktop/scroll_top.xml +++ b/widget/testdata/menu/desktop/scroll_top.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/desktop/traverse_first_active.xml b/widget/testdata/menu/desktop/traverse_first_active.xml index 204aa37558..dba03c69e2 100644 --- a/widget/testdata/menu/desktop/traverse_first_active.xml +++ b/widget/testdata/menu/desktop/traverse_first_active.xml @@ -1,6 +1,6 @@ - + - + @@ -21,14 +21,14 @@ Foo - + Bar - + - + Baz diff --git a/widget/testdata/menu/desktop/traverse_initial.xml b/widget/testdata/menu/desktop/traverse_initial.xml index 4b6dbb49ba..a228b0f6fb 100644 --- a/widget/testdata/menu/desktop/traverse_initial.xml +++ b/widget/testdata/menu/desktop/traverse_initial.xml @@ -1,6 +1,6 @@ - + - + @@ -20,14 +20,14 @@ Foo - + Bar - + - + Baz diff --git a/widget/testdata/menu/desktop/traverse_second_active.xml b/widget/testdata/menu/desktop/traverse_second_active.xml index 8a7ac91ae4..dd181e4d10 100644 --- a/widget/testdata/menu/desktop/traverse_second_active.xml +++ b/widget/testdata/menu/desktop/traverse_second_active.xml @@ -1,6 +1,6 @@ - + - + @@ -20,15 +20,15 @@ Foo - + Bar - + - + Baz diff --git a/widget/testdata/menu/desktop/traverse_submenu_first_active.xml b/widget/testdata/menu/desktop/traverse_submenu_first_active.xml index 8262d5a9c5..5e90d80f6f 100644 --- a/widget/testdata/menu/desktop/traverse_submenu_first_active.xml +++ b/widget/testdata/menu/desktop/traverse_submenu_first_active.xml @@ -1,6 +1,6 @@ - + - + @@ -20,15 +20,15 @@ Foo - + Bar - + - + Baz @@ -36,7 +36,7 @@ - + diff --git a/widget/testdata/menu/desktop/traverse_submenu_second_active.xml b/widget/testdata/menu/desktop/traverse_submenu_second_active.xml index 690bb03174..ad4cd0bc9f 100644 --- a/widget/testdata/menu/desktop/traverse_submenu_second_active.xml +++ b/widget/testdata/menu/desktop/traverse_submenu_second_active.xml @@ -1,6 +1,6 @@ - + - + @@ -20,15 +20,15 @@ Foo - + Bar - + - + Baz @@ -36,7 +36,7 @@ - + diff --git a/widget/testdata/menu/desktop/traverse_third_active.xml b/widget/testdata/menu/desktop/traverse_third_active.xml index d4999f75a9..78e80bafa2 100644 --- a/widget/testdata/menu/desktop/traverse_third_active.xml +++ b/widget/testdata/menu/desktop/traverse_third_active.xml @@ -1,6 +1,6 @@ - + - + @@ -20,14 +20,14 @@ Foo - + Bar - + - + diff --git a/widget/testdata/menu/mobile/drag_bottom.xml b/widget/testdata/menu/mobile/drag_bottom.xml index fef1fadb46..c8fbcd4043 100644 --- a/widget/testdata/menu/mobile/drag_bottom.xml +++ b/widget/testdata/menu/mobile/drag_bottom.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/mobile/drag_middle.xml b/widget/testdata/menu/mobile/drag_middle.xml index 5c0c69f979..b07e80d798 100644 --- a/widget/testdata/menu/mobile/drag_middle.xml +++ b/widget/testdata/menu/mobile/drag_middle.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/mobile/drag_top.xml b/widget/testdata/menu/mobile/drag_top.xml index a395d1c309..efe483f09d 100644 --- a/widget/testdata/menu/mobile/drag_top.xml +++ b/widget/testdata/menu/mobile/drag_top.xml @@ -1,9 +1,9 @@ - + - + - + diff --git a/widget/testdata/menu/mobile/layout_background_reset.xml b/widget/testdata/menu/mobile/layout_background_reset.xml index b14ed61baa..1985e5cccf 100644 --- a/widget/testdata/menu/mobile/layout_background_reset.xml +++ b/widget/testdata/menu/mobile/layout_background_reset.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + diff --git a/widget/testdata/menu/mobile/layout_no_space_on_both_sides.xml b/widget/testdata/menu/mobile/layout_no_space_on_both_sides.xml index 7dc8eaef2b..fb5b48eaf3 100644 --- a/widget/testdata/menu/mobile/layout_no_space_on_both_sides.xml +++ b/widget/testdata/menu/mobile/layout_no_space_on_both_sides.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + @@ -52,17 +52,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/mobile/layout_no_space_on_right.xml b/widget/testdata/menu/mobile/layout_no_space_on_right.xml index d6124ca1c2..3511c8ad7f 100644 --- a/widget/testdata/menu/mobile/layout_no_space_on_right.xml +++ b/widget/testdata/menu/mobile/layout_no_space_on_right.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + @@ -52,17 +52,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/mobile/layout_normal.png b/widget/testdata/menu/mobile/layout_normal.png index 19d901476a..778f5db9ad 100644 Binary files a/widget/testdata/menu/mobile/layout_normal.png and b/widget/testdata/menu/mobile/layout_normal.png differ diff --git a/widget/testdata/menu/mobile/layout_normal.xml b/widget/testdata/menu/mobile/layout_normal.xml index b14ed61baa..1985e5cccf 100644 --- a/widget/testdata/menu/mobile/layout_normal.xml +++ b/widget/testdata/menu/mobile/layout_normal.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + diff --git a/widget/testdata/menu/mobile/layout_normal_with_submenus.xml b/widget/testdata/menu/mobile/layout_normal_with_submenus.xml index 32eaa42360..aefdf5e83f 100644 --- a/widget/testdata/menu/mobile/layout_normal_with_submenus.xml +++ b/widget/testdata/menu/mobile/layout_normal_with_submenus.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + @@ -52,17 +52,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + diff --git a/widget/testdata/menu/mobile/layout_theme_changed.png b/widget/testdata/menu/mobile/layout_theme_changed.png index c51dd8c670..046537172a 100644 Binary files a/widget/testdata/menu/mobile/layout_theme_changed.png and b/widget/testdata/menu/mobile/layout_theme_changed.png differ diff --git a/widget/testdata/menu/mobile/layout_theme_changed.xml b/widget/testdata/menu/mobile/layout_theme_changed.xml index b14ed61baa..1985e5cccf 100644 --- a/widget/testdata/menu/mobile/layout_theme_changed.xml +++ b/widget/testdata/menu/mobile/layout_theme_changed.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + diff --git a/widget/testdata/menu/mobile/layout_window_too_short.xml b/widget/testdata/menu/mobile/layout_window_too_short.xml index 2fe117b3bf..0ce0f8f971 100644 --- a/widget/testdata/menu/mobile/layout_window_too_short.xml +++ b/widget/testdata/menu/mobile/layout_window_too_short.xml @@ -3,19 +3,19 @@ - - - + + + - - - - - + + + + + - + @@ -23,21 +23,21 @@ A - + B (long) C - + - + - + diff --git a/widget/testdata/menu/mobile/layout_window_too_short_for_submenu.xml b/widget/testdata/menu/mobile/layout_window_too_short_for_submenu.xml index 2bdc0316b3..e63f1db9e0 100644 --- a/widget/testdata/menu/mobile/layout_window_too_short_for_submenu.xml +++ b/widget/testdata/menu/mobile/layout_window_too_short_for_submenu.xml @@ -3,7 +3,7 @@ - + @@ -23,14 +23,14 @@ A - + B (long) C - + @@ -52,17 +52,17 @@ subitem A - + subitem B - - + + subitem C (long) - - + + @@ -84,7 +84,7 @@ subsubitem A (long) - + subsubitem B diff --git a/widget/testdata/menu/refresh_2nd_checkmark.xml b/widget/testdata/menu/refresh_2nd_checkmark.xml index d251ef3cd1..7e873d7a09 100644 --- a/widget/testdata/menu/refresh_2nd_checkmark.xml +++ b/widget/testdata/menu/refresh_2nd_checkmark.xml @@ -1,6 +1,6 @@ - + - + @@ -20,7 +20,7 @@ Foo - + Bar @@ -29,11 +29,11 @@ - + Baz - + diff --git a/widget/testdata/menu/refresh_checkmark.xml b/widget/testdata/menu/refresh_checkmark.xml index 30be87fdad..e6a962571d 100644 --- a/widget/testdata/menu/refresh_checkmark.xml +++ b/widget/testdata/menu/refresh_checkmark.xml @@ -1,6 +1,6 @@ - + - + @@ -20,7 +20,7 @@ Foo - + Bar @@ -28,11 +28,11 @@ - + Baz - + diff --git a/widget/testdata/menu/refresh_disabled.xml b/widget/testdata/menu/refresh_disabled.xml index 9cccbebd60..4e8688ddc6 100644 --- a/widget/testdata/menu/refresh_disabled.xml +++ b/widget/testdata/menu/refresh_disabled.xml @@ -1,6 +1,6 @@ - + - + @@ -20,7 +20,7 @@ Foo - + Bar @@ -28,7 +28,7 @@ - + Baz diff --git a/widget/testdata/menu/refresh_initial.xml b/widget/testdata/menu/refresh_initial.xml index c620d85e41..508301020e 100644 --- a/widget/testdata/menu/refresh_initial.xml +++ b/widget/testdata/menu/refresh_initial.xml @@ -1,6 +1,6 @@ - + - + @@ -20,15 +20,15 @@ Foo - + Bar - - + + - + Baz diff --git a/widget/testdata/password_entry/concealed.xml b/widget/testdata/password_entry/concealed.xml index 4aeb653138..d940f0ce1c 100644 --- a/widget/testdata/password_entry/concealed.xml +++ b/widget/testdata/password_entry/concealed.xml @@ -6,13 +6,13 @@ - ••••••• + •••••• - + - + diff --git a/widget/testdata/password_entry/initial.xml b/widget/testdata/password_entry/initial.xml index 91a53becaf..e90fea5221 100644 --- a/widget/testdata/password_entry/initial.xml +++ b/widget/testdata/password_entry/initial.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/password_entry/obfuscation_typed.xml b/widget/testdata/password_entry/obfuscation_typed.xml index 4aeb653138..c50cf399d4 100644 --- a/widget/testdata/password_entry/obfuscation_typed.xml +++ b/widget/testdata/password_entry/obfuscation_typed.xml @@ -12,7 +12,7 @@ - + diff --git a/widget/testdata/password_entry/placeholder_initial.xml b/widget/testdata/password_entry/placeholder_initial.xml index e053dd68db..788a91c1bd 100644 --- a/widget/testdata/password_entry/placeholder_initial.xml +++ b/widget/testdata/password_entry/placeholder_initial.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/password_entry/placeholder_typed.xml b/widget/testdata/password_entry/placeholder_typed.xml index 4aeb653138..c50cf399d4 100644 --- a/widget/testdata/password_entry/placeholder_typed.xml +++ b/widget/testdata/password_entry/placeholder_typed.xml @@ -12,7 +12,7 @@ - + diff --git a/widget/testdata/password_entry/revealed.xml b/widget/testdata/password_entry/revealed.xml index 53198d4fa4..e72e2674f7 100644 --- a/widget/testdata/password_entry/revealed.xml +++ b/widget/testdata/password_entry/revealed.xml @@ -6,13 +6,13 @@ - Hié™שרה + Secret - + - + diff --git a/widget/testdata/popup/modal-onshow-theme-changed.png b/widget/testdata/popup/modal-onshow-theme-changed.png index 057105b490..807d7091ce 100644 Binary files a/widget/testdata/popup/modal-onshow-theme-changed.png and b/widget/testdata/popup/modal-onshow-theme-changed.png differ diff --git a/widget/testdata/popup/modal-onshow-theme-default.png b/widget/testdata/popup/modal-onshow-theme-default.png index f3c5d2f83a..51f18a441b 100644 Binary files a/widget/testdata/popup/modal-onshow-theme-default.png and b/widget/testdata/popup/modal-onshow-theme-default.png differ diff --git a/widget/testdata/popup/normal-onshow-theme-changed.png b/widget/testdata/popup/normal-onshow-theme-changed.png index 6409c35d4b..c5f21b9201 100644 Binary files a/widget/testdata/popup/normal-onshow-theme-changed.png and b/widget/testdata/popup/normal-onshow-theme-changed.png differ diff --git a/widget/testdata/popup/normal-onshow-theme-default.png b/widget/testdata/popup/normal-onshow-theme-default.png index 74fd593e43..4419e06030 100644 Binary files a/widget/testdata/popup/normal-onshow-theme-default.png and b/widget/testdata/popup/normal-onshow-theme-default.png differ diff --git a/widget/testdata/popup_menu/canvas_too_small.xml b/widget/testdata/popup_menu/canvas_too_small.xml index a9e9690da7..7eedbe2962 100644 --- a/widget/testdata/popup_menu/canvas_too_small.xml +++ b/widget/testdata/popup_menu/canvas_too_small.xml @@ -3,22 +3,22 @@ - - - + + + - - - - - + + + + + - - - - + + + + Option A diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_active.xml index 00a0a1b3ec..1603dc0cb7 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_active.xml index 6904d425da..56722b2794 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + @@ -51,7 +51,7 @@ Sub Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_sub_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_sub_active.xml index 874461ab60..82af199de6 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_sub_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_first_sub_sub_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + @@ -51,7 +51,7 @@ Sub Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_active.xml index 450e93da9a..a62d93ac42 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_active.xml index 0ec5705374..b6408c8777 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + @@ -51,7 +51,7 @@ Sub Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_sub_active.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_sub_active.xml index 73cb8704c8..ee7c22f2a1 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_sub_active.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_second_sub_sub_active.xml @@ -3,7 +3,7 @@ - + @@ -25,7 +25,7 @@ Option B - + @@ -51,7 +51,7 @@ Sub Option B - + diff --git a/widget/testdata/popup_menu/desktop/kbd_ctrl_shown.xml b/widget/testdata/popup_menu/desktop/kbd_ctrl_shown.xml index 01738fbefd..5a12a7e8d9 100644 --- a/widget/testdata/popup_menu/desktop/kbd_ctrl_shown.xml +++ b/widget/testdata/popup_menu/desktop/kbd_ctrl_shown.xml @@ -3,7 +3,7 @@ - + @@ -24,7 +24,7 @@ Option B - + diff --git a/widget/testdata/popup_menu/grown.xml b/widget/testdata/popup_menu/grown.xml index 7676e5fb86..058fd3d177 100644 --- a/widget/testdata/popup_menu/grown.xml +++ b/widget/testdata/popup_menu/grown.xml @@ -3,7 +3,7 @@ - + diff --git a/widget/testdata/popup_menu/moved.xml b/widget/testdata/popup_menu/moved.xml index 8763cc2bf2..34ed279ad3 100644 --- a/widget/testdata/popup_menu/moved.xml +++ b/widget/testdata/popup_menu/moved.xml @@ -3,7 +3,7 @@ - + diff --git a/widget/testdata/popup_menu/no_space_down.xml b/widget/testdata/popup_menu/no_space_down.xml index c0207cbf77..fa773ba241 100644 --- a/widget/testdata/popup_menu/no_space_down.xml +++ b/widget/testdata/popup_menu/no_space_down.xml @@ -3,8 +3,8 @@ - - + + diff --git a/widget/testdata/popup_menu/no_space_right.xml b/widget/testdata/popup_menu/no_space_right.xml index c4ef13f93c..d1159604e7 100644 --- a/widget/testdata/popup_menu/no_space_right.xml +++ b/widget/testdata/popup_menu/no_space_right.xml @@ -3,8 +3,8 @@ - - + + diff --git a/widget/testdata/popup_menu/shown.xml b/widget/testdata/popup_menu/shown.xml index ad244eef31..f3ebbc85c7 100644 --- a/widget/testdata/popup_menu/shown.xml +++ b/widget/testdata/popup_menu/shown.xml @@ -3,7 +3,7 @@ - + diff --git a/widget/testdata/popup_menu/shown_at_pos.xml b/widget/testdata/popup_menu/shown_at_pos.xml index fe96086ff6..94ea4ff628 100644 --- a/widget/testdata/popup_menu/shown_at_pos.xml +++ b/widget/testdata/popup_menu/shown_at_pos.xml @@ -3,7 +3,7 @@ - + diff --git a/widget/testdata/select/desktop/center.xml b/widget/testdata/select/desktop/center.xml index 6e23bfebf4..8554e32e4b 100644 --- a/widget/testdata/select/desktop/center.xml +++ b/widget/testdata/select/desktop/center.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_empty_expanded.xml b/widget/testdata/select/desktop/layout_empty_expanded.xml index 89a1c91202..de49b4ecd9 100644 --- a/widget/testdata/select/desktop/layout_empty_expanded.xml +++ b/widget/testdata/select/desktop/layout_empty_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_empty_expanded_placeholder.xml b/widget/testdata/select/desktop/layout_empty_expanded_placeholder.xml index eac90703e0..e9cc67d69b 100644 --- a/widget/testdata/select/desktop/layout_empty_expanded_placeholder.xml +++ b/widget/testdata/select/desktop/layout_empty_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_multiple_expanded.xml b/widget/testdata/select/desktop/layout_multiple_expanded.xml index ffd188cc5e..9dd2180408 100644 --- a/widget/testdata/select/desktop/layout_multiple_expanded.xml +++ b/widget/testdata/select/desktop/layout_multiple_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_multiple_expanded_placeholder.xml b/widget/testdata/select/desktop/layout_multiple_expanded_placeholder.xml index 4cb46a4b0f..59ec939be5 100644 --- a/widget/testdata/select/desktop/layout_multiple_expanded_placeholder.xml +++ b/widget/testdata/select/desktop/layout_multiple_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_multiple_expanded_selected.xml b/widget/testdata/select/desktop/layout_multiple_expanded_selected.xml index 67449e1f10..18bfb361b1 100644 --- a/widget/testdata/select/desktop/layout_multiple_expanded_selected.xml +++ b/widget/testdata/select/desktop/layout_multiple_expanded_selected.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_multiple_expanded_selected_placeholder.xml b/widget/testdata/select/desktop/layout_multiple_expanded_selected_placeholder.xml index d421573824..39139e3bdc 100644 --- a/widget/testdata/select/desktop/layout_multiple_expanded_selected_placeholder.xml +++ b/widget/testdata/select/desktop/layout_multiple_expanded_selected_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_single_expanded.xml b/widget/testdata/select/desktop/layout_single_expanded.xml index 7ecc175cec..1505d9daa9 100644 --- a/widget/testdata/select/desktop/layout_single_expanded.xml +++ b/widget/testdata/select/desktop/layout_single_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_single_expanded_placeholder.xml b/widget/testdata/select/desktop/layout_single_expanded_placeholder.xml index 46ffc10d73..c05838c4c5 100644 --- a/widget/testdata/select/desktop/layout_single_expanded_placeholder.xml +++ b/widget/testdata/select/desktop/layout_single_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_single_expanded_selected.xml b/widget/testdata/select/desktop/layout_single_expanded_selected.xml index 525230be5e..ec634b8d11 100644 --- a/widget/testdata/select/desktop/layout_single_expanded_selected.xml +++ b/widget/testdata/select/desktop/layout_single_expanded_selected.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/layout_single_expanded_selected_placeholder.xml b/widget/testdata/select/desktop/layout_single_expanded_selected_placeholder.xml index 32e449c776..b4ea2e54a8 100644 --- a/widget/testdata/select/desktop/layout_single_expanded_selected_placeholder.xml +++ b/widget/testdata/select/desktop/layout_single_expanded_selected_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/desktop/move_moved.xml b/widget/testdata/select/desktop/move_moved.xml index 89c77578db..d8970d07ad 100644 --- a/widget/testdata/select/desktop/move_moved.xml +++ b/widget/testdata/select/desktop/move_moved.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/desktop/move_tapped.xml b/widget/testdata/select/desktop/move_tapped.xml index a77a5ccdce..1bc0dc77e8 100644 --- a/widget/testdata/select/desktop/move_tapped.xml +++ b/widget/testdata/select/desktop/move_tapped.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/desktop/tap_animation.png b/widget/testdata/select/desktop/tap_animation.png index 18dfb851b4..8284e1c95c 100644 Binary files a/widget/testdata/select/desktop/tap_animation.png and b/widget/testdata/select/desktop/tap_animation.png differ diff --git a/widget/testdata/select/desktop/tapped.xml b/widget/testdata/select/desktop/tapped.xml index dd334b3cf0..e458b45278 100644 --- a/widget/testdata/select/desktop/tapped.xml +++ b/widget/testdata/select/desktop/tapped.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/desktop/tapped_constrained.xml b/widget/testdata/select/desktop/tapped_constrained.xml index 8e268ea568..c8a4d4645f 100644 --- a/widget/testdata/select/desktop/tapped_constrained.xml +++ b/widget/testdata/select/desktop/tapped_constrained.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/desktop/theme_changed.png b/widget/testdata/select/desktop/theme_changed.png index 7855eeee53..489dace2f2 100644 Binary files a/widget/testdata/select/desktop/theme_changed.png and b/widget/testdata/select/desktop/theme_changed.png differ diff --git a/widget/testdata/select/desktop/trailing.xml b/widget/testdata/select/desktop/trailing.xml index af8d9552c8..4a508145ea 100644 --- a/widget/testdata/select/desktop/trailing.xml +++ b/widget/testdata/select/desktop/trailing.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/disabled.xml b/widget/testdata/select/disabled.xml index 1db05e3504..e38758d822 100644 --- a/widget/testdata/select/disabled.xml +++ b/widget/testdata/select/disabled.xml @@ -2,7 +2,7 @@ - + (Select one) diff --git a/widget/testdata/select/kbdctrl_none_selected_popup.xml b/widget/testdata/select/kbdctrl_none_selected_popup.xml index 61f07a3f3b..a7241c9c3e 100644 --- a/widget/testdata/select/kbdctrl_none_selected_popup.xml +++ b/widget/testdata/select/kbdctrl_none_selected_popup.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/center.xml b/widget/testdata/select/mobile/center.xml index 56b720b328..39ba7a7342 100644 --- a/widget/testdata/select/mobile/center.xml +++ b/widget/testdata/select/mobile/center.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_empty_expanded.xml b/widget/testdata/select/mobile/layout_empty_expanded.xml index 805dd29a86..3eedb23ae8 100644 --- a/widget/testdata/select/mobile/layout_empty_expanded.xml +++ b/widget/testdata/select/mobile/layout_empty_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_empty_expanded_placeholder.xml b/widget/testdata/select/mobile/layout_empty_expanded_placeholder.xml index 8420933255..a5a9bd5a60 100644 --- a/widget/testdata/select/mobile/layout_empty_expanded_placeholder.xml +++ b/widget/testdata/select/mobile/layout_empty_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_multiple_expanded.xml b/widget/testdata/select/mobile/layout_multiple_expanded.xml index a2245801d5..80b0712c51 100644 --- a/widget/testdata/select/mobile/layout_multiple_expanded.xml +++ b/widget/testdata/select/mobile/layout_multiple_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_multiple_expanded_placeholder.xml b/widget/testdata/select/mobile/layout_multiple_expanded_placeholder.xml index 4f825528f0..b4a6bf5465 100644 --- a/widget/testdata/select/mobile/layout_multiple_expanded_placeholder.xml +++ b/widget/testdata/select/mobile/layout_multiple_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_multiple_expanded_selected.xml b/widget/testdata/select/mobile/layout_multiple_expanded_selected.xml index f4a56cf266..981c30de72 100644 --- a/widget/testdata/select/mobile/layout_multiple_expanded_selected.xml +++ b/widget/testdata/select/mobile/layout_multiple_expanded_selected.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_multiple_expanded_selected_placeholder.xml b/widget/testdata/select/mobile/layout_multiple_expanded_selected_placeholder.xml index c989b1f63f..e9b6ea463a 100644 --- a/widget/testdata/select/mobile/layout_multiple_expanded_selected_placeholder.xml +++ b/widget/testdata/select/mobile/layout_multiple_expanded_selected_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_single_expanded.xml b/widget/testdata/select/mobile/layout_single_expanded.xml index 98f080550a..739c3d74ad 100644 --- a/widget/testdata/select/mobile/layout_single_expanded.xml +++ b/widget/testdata/select/mobile/layout_single_expanded.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_single_expanded_placeholder.xml b/widget/testdata/select/mobile/layout_single_expanded_placeholder.xml index 5eba20c119..440cdfa7a3 100644 --- a/widget/testdata/select/mobile/layout_single_expanded_placeholder.xml +++ b/widget/testdata/select/mobile/layout_single_expanded_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_single_expanded_selected.xml b/widget/testdata/select/mobile/layout_single_expanded_selected.xml index 42cb894a25..f4657c05cf 100644 --- a/widget/testdata/select/mobile/layout_single_expanded_selected.xml +++ b/widget/testdata/select/mobile/layout_single_expanded_selected.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/layout_single_expanded_selected_placeholder.xml b/widget/testdata/select/mobile/layout_single_expanded_selected_placeholder.xml index 9fd05ef675..c4237bd6f1 100644 --- a/widget/testdata/select/mobile/layout_single_expanded_selected_placeholder.xml +++ b/widget/testdata/select/mobile/layout_single_expanded_selected_placeholder.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select/mobile/move_moved.xml b/widget/testdata/select/mobile/move_moved.xml index e15d8800fa..dad9d1c541 100644 --- a/widget/testdata/select/mobile/move_moved.xml +++ b/widget/testdata/select/mobile/move_moved.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/mobile/move_tapped.xml b/widget/testdata/select/mobile/move_tapped.xml index b52e670d12..18ec0f2581 100644 --- a/widget/testdata/select/mobile/move_tapped.xml +++ b/widget/testdata/select/mobile/move_tapped.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/mobile/tap_animation.png b/widget/testdata/select/mobile/tap_animation.png index 2491b0551d..552607bd67 100644 Binary files a/widget/testdata/select/mobile/tap_animation.png and b/widget/testdata/select/mobile/tap_animation.png differ diff --git a/widget/testdata/select/mobile/tapped.xml b/widget/testdata/select/mobile/tapped.xml index bde8990320..530b92b216 100644 --- a/widget/testdata/select/mobile/tapped.xml +++ b/widget/testdata/select/mobile/tapped.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/mobile/tapped_constrained.xml b/widget/testdata/select/mobile/tapped_constrained.xml index c8a1890166..7d6ae39b11 100644 --- a/widget/testdata/select/mobile/tapped_constrained.xml +++ b/widget/testdata/select/mobile/tapped_constrained.xml @@ -12,8 +12,8 @@ - - + + diff --git a/widget/testdata/select/mobile/theme_changed.png b/widget/testdata/select/mobile/theme_changed.png index 197350e944..af4d790edf 100644 Binary files a/widget/testdata/select/mobile/theme_changed.png and b/widget/testdata/select/mobile/theme_changed.png differ diff --git a/widget/testdata/select/mobile/trailing.xml b/widget/testdata/select/mobile/trailing.xml index b225ff3248..9f5f1c2cdf 100644 --- a/widget/testdata/select/mobile/trailing.xml +++ b/widget/testdata/select/mobile/trailing.xml @@ -14,8 +14,8 @@ - - + + diff --git a/widget/testdata/select_entry/disableable_enabled_opened.xml b/widget/testdata/select_entry/disableable_enabled_opened.xml index 437628c722..a406a38880 100644 --- a/widget/testdata/select_entry/disableable_enabled_opened.xml +++ b/widget/testdata/select_entry/disableable_enabled_opened.xml @@ -21,8 +21,8 @@ - - + + diff --git a/widget/testdata/select_entry/dropdown_B_opened.xml b/widget/testdata/select_entry/dropdown_B_opened.xml index 9981e8d328..598666d4ce 100644 --- a/widget/testdata/select_entry/dropdown_B_opened.xml +++ b/widget/testdata/select_entry/dropdown_B_opened.xml @@ -18,8 +18,8 @@ - - + + diff --git a/widget/testdata/select_entry/dropdown_empty_opened.xml b/widget/testdata/select_entry/dropdown_empty_opened.xml index 437628c722..a406a38880 100644 --- a/widget/testdata/select_entry/dropdown_empty_opened.xml +++ b/widget/testdata/select_entry/dropdown_empty_opened.xml @@ -21,8 +21,8 @@ - - + + diff --git a/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml b/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml index dcc8b773cf..3e730ba386 100644 --- a/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml +++ b/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml @@ -21,8 +21,8 @@ - - + + diff --git a/widget/testdata/select_entry/dropdown_empty_setopts.xml b/widget/testdata/select_entry/dropdown_empty_setopts.xml index 8d9357da46..0dde8f3b42 100644 --- a/widget/testdata/select_entry/dropdown_empty_setopts.xml +++ b/widget/testdata/select_entry/dropdown_empty_setopts.xml @@ -21,8 +21,8 @@ - - + + diff --git a/widget/testdata/table/desktop/hovered.xml b/widget/testdata/table/desktop/hovered.xml index df70a312b7..70012410aa 100644 --- a/widget/testdata/table/desktop/hovered.xml +++ b/widget/testdata/table/desktop/hovered.xml @@ -17,13 +17,13 @@ - + - + - + diff --git a/widget/testdata/table/desktop/hovered_out.xml b/widget/testdata/table/desktop/hovered_out.xml index 37127238e9..84d90eaba9 100644 --- a/widget/testdata/table/desktop/hovered_out.xml +++ b/widget/testdata/table/desktop/hovered_out.xml @@ -16,10 +16,10 @@ - + - + diff --git a/widget/testdata/table/just_headers.xml b/widget/testdata/table/just_headers.xml index 3a48aa8369..634d34a451 100644 --- a/widget/testdata/table/just_headers.xml +++ b/widget/testdata/table/just_headers.xml @@ -3,7 +3,7 @@ - + A @@ -23,22 +23,22 @@ - + - - + + - + - + diff --git a/widget/testdata/table/resize.png b/widget/testdata/table/resize.png index eda3424122..83eaabf2ac 100644 Binary files a/widget/testdata/table/resize.png and b/widget/testdata/table/resize.png differ diff --git a/widget/testdata/table/selected.xml b/widget/testdata/table/selected.xml index 7c3a9bf76b..b127f0577f 100644 --- a/widget/testdata/table/selected.xml +++ b/widget/testdata/table/selected.xml @@ -17,19 +17,19 @@ - + - + - + - + - + diff --git a/widget/testdata/table/selected_scrolled.xml b/widget/testdata/table/selected_scrolled.xml index d95b2139b1..82ae5ae6e7 100644 --- a/widget/testdata/table/selected_scrolled.xml +++ b/widget/testdata/table/selected_scrolled.xml @@ -17,19 +17,19 @@ - + - + - + - + - + diff --git a/widget/testdata/table/theme_changed.png b/widget/testdata/table/theme_changed.png index 27f68b643e..51e108fdf5 100644 Binary files a/widget/testdata/table/theme_changed.png and b/widget/testdata/table/theme_changed.png differ diff --git a/widget/testdata/table/theme_initial.png b/widget/testdata/table/theme_initial.png index a9949517be..f92541d59d 100644 Binary files a/widget/testdata/table/theme_initial.png and b/widget/testdata/table/theme_initial.png differ diff --git a/widget/testdata/tree/layout_multiple.xml b/widget/testdata/tree/layout_multiple.xml index f163d8ad81..ad4493266f 100644 --- a/widget/testdata/tree/layout_multiple.xml +++ b/widget/testdata/tree/layout_multiple.xml @@ -14,7 +14,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_branch.xml b/widget/testdata/tree/layout_multiple_branch.xml index c3e65eb083..951678ac09 100644 --- a/widget/testdata/tree/layout_multiple_branch.xml +++ b/widget/testdata/tree/layout_multiple_branch.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_branch_opened.xml b/widget/testdata/tree/layout_multiple_branch_opened.xml index 6025fa6c7c..9670887590 100644 --- a/widget/testdata/tree/layout_multiple_branch_opened.xml +++ b/widget/testdata/tree/layout_multiple_branch_opened.xml @@ -14,7 +14,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -37,7 +37,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -60,7 +60,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_branch_opened_leaf_selected.xml b/widget/testdata/tree/layout_multiple_branch_opened_leaf_selected.xml index 94ecf8790a..26ce829e2b 100644 --- a/widget/testdata/tree/layout_multiple_branch_opened_leaf_selected.xml +++ b/widget/testdata/tree/layout_multiple_branch_opened_leaf_selected.xml @@ -14,7 +14,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -37,7 +37,7 @@ - + @@ -48,7 +48,7 @@ - + @@ -61,7 +61,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_branch_opened_selected.xml b/widget/testdata/tree/layout_multiple_branch_opened_selected.xml index c293b06983..9c429af46e 100644 --- a/widget/testdata/tree/layout_multiple_branch_opened_selected.xml +++ b/widget/testdata/tree/layout_multiple_branch_opened_selected.xml @@ -14,7 +14,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -38,7 +38,7 @@ - + @@ -48,7 +48,7 @@ - + @@ -61,7 +61,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_branch_selected.xml b/widget/testdata/tree/layout_multiple_branch_selected.xml index 27f485efa6..aab25776fc 100644 --- a/widget/testdata/tree/layout_multiple_branch_selected.xml +++ b/widget/testdata/tree/layout_multiple_branch_selected.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_leaf.xml b/widget/testdata/tree/layout_multiple_leaf.xml index 751beabf7b..9a3a0a97c8 100644 --- a/widget/testdata/tree/layout_multiple_leaf.xml +++ b/widget/testdata/tree/layout_multiple_leaf.xml @@ -11,7 +11,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -31,7 +31,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_leaf_selected.xml b/widget/testdata/tree/layout_multiple_leaf_selected.xml index 356ce3fa01..2aa63b8847 100644 --- a/widget/testdata/tree/layout_multiple_leaf_selected.xml +++ b/widget/testdata/tree/layout_multiple_leaf_selected.xml @@ -11,7 +11,7 @@ - + @@ -22,7 +22,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/widget/testdata/tree/layout_multiple_selected.xml b/widget/testdata/tree/layout_multiple_selected.xml index 0f42539225..f56138d952 100644 --- a/widget/testdata/tree/layout_multiple_selected.xml +++ b/widget/testdata/tree/layout_multiple_selected.xml @@ -14,7 +14,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/widget/testdata/tree/layout_single_branch_opened.xml b/widget/testdata/tree/layout_single_branch_opened.xml index a4c1613dd0..41f13bb606 100644 --- a/widget/testdata/tree/layout_single_branch_opened.xml +++ b/widget/testdata/tree/layout_single_branch_opened.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/layout_single_branch_opened_leaf_selected.xml b/widget/testdata/tree/layout_single_branch_opened_leaf_selected.xml index 9cddaa1e58..4752068030 100644 --- a/widget/testdata/tree/layout_single_branch_opened_leaf_selected.xml +++ b/widget/testdata/tree/layout_single_branch_opened_leaf_selected.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/layout_single_branch_opened_selected.xml b/widget/testdata/tree/layout_single_branch_opened_selected.xml index c2e2cf3df7..79411a5beb 100644 --- a/widget/testdata/tree/layout_single_branch_opened_selected.xml +++ b/widget/testdata/tree/layout_single_branch_opened_selected.xml @@ -15,7 +15,7 @@ - + diff --git a/widget/testdata/tree/move_initial.xml b/widget/testdata/tree/move_initial.xml index 4a359777cd..71ae4c57a0 100644 --- a/widget/testdata/tree/move_initial.xml +++ b/widget/testdata/tree/move_initial.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/move_moved.xml b/widget/testdata/tree/move_moved.xml index 3206e4e083..9ea4ab28cb 100644 --- a/widget/testdata/tree/move_moved.xml +++ b/widget/testdata/tree/move_moved.xml @@ -14,7 +14,7 @@ - + diff --git a/widget/testdata/tree/theme_changed.png b/widget/testdata/tree/theme_changed.png index e8219d098d..e9547f52a3 100644 Binary files a/widget/testdata/tree/theme_changed.png and b/widget/testdata/tree/theme_changed.png differ diff --git a/widget/testdata/tree/theme_initial.png b/widget/testdata/tree/theme_initial.png index e681f9c0ad..c2d568fe13 100644 Binary files a/widget/testdata/tree/theme_initial.png and b/widget/testdata/tree/theme_initial.png differ diff --git a/widget/textgrid.go b/widget/textgrid.go index 38735f72a3..48d62637fd 100644 --- a/widget/textgrid.go +++ b/widget/textgrid.go @@ -43,12 +43,15 @@ type TextGridRow struct { // TextGridStyle defines a style that can be applied to a TextGrid cell. type TextGridStyle interface { + Style() fyne.TextStyle TextColor() color.Color BackgroundColor() color.Color } // CustomTextGridStyle is a utility type for those not wanting to define their own style types. type CustomTextGridStyle struct { + // Since: 2.5 + TextStyle fyne.TextStyle FGColor, BGColor color.Color } @@ -62,6 +65,11 @@ func (c *CustomTextGridStyle) BackgroundColor() color.Color { return c.BGColor } +// Style is the text style a cell should use. +func (c *CustomTextGridStyle) Style() fyne.TextStyle { + return c.TextStyle +} + // TextGrid is a monospaced grid of characters. // This is designed to be used by a text editor, code preview or terminal emulator. type TextGrid struct { @@ -350,12 +358,15 @@ func (t *textGridRenderer) appendTextCell(str rune) { text.TextStyle.Monospace = true bg := canvas.NewRectangle(color.Transparent) - t.objects = append(t.objects, bg, text) + + ul := canvas.NewLine(color.Transparent) + + t.objects = append(t.objects, bg, text, ul) } func (t *textGridRenderer) refreshCell(row, col int) { pos := row*t.cols + col - if pos*2+1 >= len(t.objects) { + if pos*3+1 >= len(t.objects) { return } @@ -367,26 +378,50 @@ func (t *textGridRenderer) setCellRune(str rune, pos int, style, rowStyle TextGr if str == 0 { str = ' ' } + rect := t.objects[pos*3].(*canvas.Rectangle) + text := t.objects[pos*3+1].(*canvas.Text) + underline := t.objects[pos*3+2].(*canvas.Line) th := t.text.Theme() v := fyne.CurrentApp().Settings().ThemeVariant() - - text := t.objects[pos*2+1].(*canvas.Text) - text.TextSize = th.Size(theme.SizeNameText) fg := th.Color(theme.ColorNameForeground, v) + text.TextSize = th.Size(theme.SizeNameText) + + var underlineStrokeWidth float32 = 1 + var underlineStrokeColor color.Color = color.Transparent + textStyle := fyne.TextStyle{} + if style != nil { + textStyle = style.Style() + } else if rowStyle != nil { + textStyle = rowStyle.Style() + } + if textStyle.Bold { + underlineStrokeWidth = 2 + } + if textStyle.Underline { + underlineStrokeColor = fg + } + textStyle.Monospace = true + if style != nil && style.TextColor() != nil { fg = style.TextColor() } else if rowStyle != nil && rowStyle.TextColor() != nil { fg = rowStyle.TextColor() } + newStr := string(str) - if text.Text != newStr || text.Color != fg { + if text.Text != newStr || text.Color != fg || textStyle != text.TextStyle { text.Text = newStr text.Color = fg + text.TextStyle = textStyle t.refresh(text) } - rect := t.objects[pos*2].(*canvas.Rectangle) + if underlineStrokeWidth != underline.StrokeWidth || underlineStrokeColor != underline.StrokeColor { + underline.StrokeWidth, underline.StrokeColor = underlineStrokeWidth, underlineStrokeColor + t.refresh(underline) + } + bg := color.Color(color.Transparent) if style != nil && style.BackgroundColor() != nil { bg = style.BackgroundColor() @@ -401,10 +436,10 @@ func (t *textGridRenderer) setCellRune(str rune, pos int, style, rowStyle TextGr func (t *textGridRenderer) addCellsIfRequired() { cellCount := t.cols * t.rows - if len(t.objects) == cellCount*2 { + if len(t.objects) == cellCount*3 { return } - for i := len(t.objects); i < cellCount*2; i += 2 { + for i := len(t.objects); i < cellCount*3; i += 3 { t.appendTextCell(' ') } } @@ -468,7 +503,7 @@ func (t *textGridRenderer) refreshGrid() { line++ } - for ; x < len(t.objects)/2; x++ { + for ; x < len(t.objects)/3; x++ { t.setCellRune(' ', x, TextGridStyleDefault, nil) // trailing cells and blank lines } } @@ -513,10 +548,17 @@ func (t *textGridRenderer) Layout(size fyne.Size) { cellPos := fyne.NewPos(0, 0) for y := 0; y < t.rows; y++ { for x := 0; x < t.cols; x++ { - t.objects[i*2+1].Move(cellPos) + // rect + t.objects[i*3].Resize(t.cellSize) + t.objects[i*3].Move(cellPos) + + // text + t.objects[i*3+1].Move(cellPos) + + // underline + t.objects[i*3+2].Move(cellPos.Add(fyne.Position{X: 0, Y: t.cellSize.Height})) + t.objects[i*3+2].Resize(fyne.Size{Width: t.cellSize.Width}) - t.objects[i*2].Resize(t.cellSize) - t.objects[i*2].Move(cellPos) cellPos.X += t.cellSize.Width i++ } diff --git a/widget/textgrid_test.go b/widget/textgrid_test.go index 0e4fa57fd1..9d37aa287b 100644 --- a/widget/textgrid_test.go +++ b/widget/textgrid_test.go @@ -15,7 +15,7 @@ import ( func TestNewTextGrid(t *testing.T) { grid := NewTextGridFromString("A") - test.WidgetRenderer(grid).Refresh() + test.TempWidgetRenderer(t, grid).Refresh() assert.Equal(t, 1, len(grid.Rows)) assert.Equal(t, 1, len(grid.Rows[0].Cells)) @@ -24,15 +24,15 @@ func TestNewTextGrid(t *testing.T) { func TestTextGrid_CreateRendererRows(t *testing.T) { grid := NewTextGrid() grid.Resize(fyne.NewSize(52, 22)) - rend := test.WidgetRenderer(grid).(*textGridRenderer) + rend := test.TempWidgetRenderer(t, grid).(*textGridRenderer) rend.Refresh() - assert.Equal(t, 12, len(rend.objects)) + assert.Equal(t, 18, len(rend.objects)) } func TestTextGrid_Row(t *testing.T) { grid := NewTextGridFromString("Ab\nC") - test.WidgetRenderer(grid).Refresh() + test.TempWidgetRenderer(t, grid).Refresh() assert.NotNil(t, grid.Row(0)) assert.Equal(t, 2, len(grid.Row(0).Cells)) @@ -41,7 +41,7 @@ func TestTextGrid_Row(t *testing.T) { func TestTextGrid_Rows(t *testing.T) { grid := NewTextGridFromString("Ab\nC") - test.WidgetRenderer(grid).Refresh() + test.TempWidgetRenderer(t, grid).Refresh() assert.Equal(t, 2, len(grid.Rows)) assert.Equal(t, 2, len(grid.Rows[0].Cells)) @@ -49,7 +49,7 @@ func TestTextGrid_Rows(t *testing.T) { func TestTextGrid_RowText(t *testing.T) { grid := NewTextGridFromString("Ab\nC") - test.WidgetRenderer(grid).Refresh() + test.TempWidgetRenderer(t, grid).Refresh() assert.Equal(t, "Ab", grid.RowText(0)) assert.Equal(t, "C", grid.RowText(1)) @@ -140,7 +140,7 @@ func TestTextGridRenderer_Resize(t *testing.T) { grid := NewTextGridFromString("1\n2") grid.ShowLineNumbers = true - renderer := test.WidgetRenderer(grid) + renderer := test.TempWidgetRenderer(t, grid) min := renderer.MinSize() grid.Resize(fyne.NewSize(100, 250)) @@ -168,7 +168,7 @@ func TestTextGridRenderer_ShowLineNumbers(t *testing.T) { func TestTextGridRender_Size(t *testing.T) { grid := NewTextGrid() grid.Resize(fyne.NewSize(30, 42)) // causes refresh - rend := test.WidgetRenderer(grid).(*textGridRenderer) + rend := test.TempWidgetRenderer(t, grid).(*textGridRenderer) assert.Equal(t, 3, rend.cols) assert.Equal(t, 2, rend.rows) @@ -203,6 +203,20 @@ func TestTextGridRender_RowColor(t *testing.T) { assertGridStyle(t, grid, "112", map[string]TextGridStyle{"1": customStyle, "2": TextGridStyleWhitespace}) } +func TestTextGridRender_Style(t *testing.T) { + grid := NewTextGridFromString("Abcd ") + boldStyle := &CustomTextGridStyle{TextStyle: fyne.TextStyle{Bold: true}} + italicStyle := &CustomTextGridStyle{TextStyle: fyne.TextStyle{Italic: true}} + boldItalicStyle := &CustomTextGridStyle{TextStyle: fyne.TextStyle{Bold: true, Italic: true}} + grid.Rows[0].Cells[1].Style = boldStyle + grid.Rows[0].Cells[2].Style = italicStyle + grid.Rows[0].Cells[3].Style = boldItalicStyle + grid.ShowWhitespace = true + grid.Resize(fyne.NewSize(56, 22)) // causes refresh + + assertGridStyle(t, grid, "0123", map[string]TextGridStyle{"1": boldStyle, "2": italicStyle, "3": boldItalicStyle}) +} + func TestTextGridRender_TextColor(t *testing.T) { grid := NewTextGridFromString("Ab ") customStyle := &CustomTextGridStyle{FGColor: color.Black} @@ -222,7 +236,7 @@ func TestTextGridRender_TextColor(t *testing.T) { func assertGridContent(t *testing.T, g *TextGrid, expected string) { lines := strings.Split(expected, "\n") - renderer := test.WidgetRenderer(g).(*textGridRenderer) + renderer := test.TempWidgetRenderer(t, g).(*textGridRenderer) for y, line := range lines { x := 0 // rune count - using index below would be offset into string bytes @@ -234,9 +248,9 @@ func assertGridContent(t *testing.T, g *TextGrid, expected string) { } } -func assertGridStyle(t *testing.T, g *TextGrid, expected string, expectedStyles map[string]TextGridStyle) { - lines := strings.Split(expected, "\n") - renderer := test.WidgetRenderer(g).(*textGridRenderer) +func assertGridStyle(t *testing.T, g *TextGrid, content string, expectedStyles map[string]TextGridStyle) { + lines := strings.Split(content, "\n") + renderer := test.TempWidgetRenderer(t, g).(*textGridRenderer) for y, line := range lines { x := 0 // rune count - using index below would be offset into string bytes @@ -247,7 +261,7 @@ func assertGridStyle(t *testing.T, g *TextGrid, expected string, expectedStyles if r == ' ' { assert.Equal(t, theme.ForegroundColor(), fg.Color) assert.Equal(t, color.Transparent, bg.FillColor) - } else { + } else if expected != nil { if expected.TextColor() == nil { assert.Equal(t, theme.ForegroundColor(), fg.Color) } else { @@ -260,12 +274,19 @@ func assertGridStyle(t *testing.T, g *TextGrid, expected string, expectedStyles assert.Equal(t, expected.BackgroundColor(), bg.FillColor) } } + + style := fyne.TextStyle{} + if expected != nil { + style = expected.Style() + } + style.Monospace = true + assert.Equal(t, style, fg.TextStyle) x++ } } } func rendererCell(r *textGridRenderer, row, col int) (*canvas.Rectangle, *canvas.Text) { - i := (row*r.cols + col) * 2 + i := (row*r.cols + col) * 3 return r.objects[i].(*canvas.Rectangle), r.objects[i+1].(*canvas.Text) } diff --git a/widget/toolbar.go b/widget/toolbar.go index 7a5f075e40..05be6c5d96 100644 --- a/widget/toolbar.go +++ b/widget/toolbar.go @@ -21,6 +21,8 @@ type ToolbarAction struct { // ToolbarObject gets a button to render this ToolbarAction func (t *ToolbarAction) ToolbarObject() fyne.CanvasObject { + t.button.Importance = LowImportance + // synchronize properties t.button.Icon = t.Icon t.button.OnTapped = t.OnActivated diff --git a/widget/toolbar_test.go b/widget/toolbar_test.go index f63fbe5cf0..5b33c60b71 100644 --- a/widget/toolbar_test.go +++ b/widget/toolbar_test.go @@ -45,7 +45,7 @@ func TestToolbar_Replace(t *testing.T) { icon := theme.ContentCutIcon() toolbar := NewToolbar(NewToolbarAction(icon, func() {})) assert.Equal(t, 1, len(toolbar.Items)) - render := test.WidgetRenderer(toolbar) + render := test.TempWidgetRenderer(t, toolbar) assert.Equal(t, icon.Name(), render.Objects()[0].(*Button).Icon.Name()) toolbar.Items[0] = NewToolbarAction(theme.HelpIcon(), func() {}) diff --git a/widget/tree.go b/widget/tree.go index e81fb108e1..83a6a6b0ce 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -30,6 +30,11 @@ type Tree struct { BaseWidget Root TreeNodeID + // HideSeparators hides the separators between tree nodes + // + // Since: 2.5 + HideSeparators bool + ChildUIDs func(uid TreeNodeID) (c []TreeNodeID) `json:"-"` // Return a sorted slice of Children TreeNodeIDs for the given Node TreeNodeID CreateNode func(branch bool) (o fyne.CanvasObject) `json:"-"` // Return a CanvasObject that can represent a Branch (if branch is true), or a Leaf (if branch is false) IsBranch func(uid TreeNodeID) (ok bool) `json:"-"` // Return true if the given TreeNodeID represents a Branch @@ -624,6 +629,7 @@ func (r *treeContentRenderer) Layout(size fyne.Size) { separatorThickness := theme.SeparatorThicknessSize() separatorSize := fyne.NewSize(width, separatorThickness) separatorOff := (pad + separatorThickness) / 2 + hideSeparators := r.treeContent.tree.HideSeparators y := float32(0) // walkAll open branches and obtain nodes to render in scroller's viewport r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) { @@ -654,10 +660,11 @@ func (r *treeContentRenderer) Layout(size fyne.Size) { } else { // Node is in viewport - if addSeparator { + if addSeparator && !hideSeparators { var separator fyne.CanvasObject if separatorCount < len(r.separators) { separator = r.separators[separatorCount] + separator.Show() // it may previously have been hidden } else { separator = NewSeparator() r.separators = append(r.separators, separator) @@ -702,6 +709,10 @@ func (r *treeContentRenderer) Layout(size fyne.Size) { y += m.Height }) + if hideSeparators { + // start below iteration from 0 to hide all separators + separatorCount = 0 + } // Hide any separators that haven't been reused for ; separatorCount < len(r.separators); separatorCount++ { r.separators[separatorCount].Hide() @@ -989,7 +1000,7 @@ func newBranch(tree *Tree, content fyne.CanvasObject) (b *branch) { func (b *branch) update(uid string, depth int) { b.treeNode.update(uid, depth) - b.icon.(*branchIcon).update(uid, depth) + b.icon.(*branchIcon).update(uid) } var _ fyne.Tappable = (*branchIcon)(nil) @@ -1021,7 +1032,7 @@ func (i *branchIcon) Tapped(*fyne.PointEvent) { i.tree.ToggleBranch(i.uid) } -func (i *branchIcon) update(uid string, depth int) { +func (i *branchIcon) update(uid string) { i.uid = uid i.Refresh() } diff --git a/widget/tree_internal_test.go b/widget/tree_internal_test.go index 5d7124b216..f0a59a0d36 100644 --- a/widget/tree_internal_test.go +++ b/widget/tree_internal_test.go @@ -878,7 +878,7 @@ func TestTree_RefreshItem(t *testing.T) { c := test.NewWindow(tree) c.Resize(fyne.NewSize(100, 100)) - r := test.WidgetRenderer(tree.scroller.Content.(*treeContent)).(*treeContentRenderer) + r := test.TempWidgetRenderer(t, tree.scroller.Content.(*treeContent)).(*treeContentRenderer) assert.Equal(t, "Leaf", r.leaves["foobar1"].content.(*Label).Text) @@ -896,13 +896,13 @@ func TestTreeNodeRenderer_BackgroundColor(t *testing.T) { tree.OpenAllBranches() t.Run("Branch", func(t *testing.T) { a := getBranch(t, tree, "A") - ar := test.WidgetRenderer(a).(*treeNodeRenderer) + ar := test.TempWidgetRenderer(t, a).(*treeNodeRenderer) assert.Equal(t, theme.HoverColor(), ar.background.FillColor) assert.False(t, ar.background.Visible()) }) t.Run("Leaf", func(t *testing.T) { b := getLeaf(t, tree, "B") - br := test.WidgetRenderer(b).(*treeNodeRenderer) + br := test.TempWidgetRenderer(t, b).(*treeNodeRenderer) assert.Equal(t, theme.HoverColor(), br.background.FillColor) assert.False(t, br.background.Visible()) }) @@ -916,7 +916,7 @@ func TestTreeNodeRenderer_BackgroundColor_Hovered(t *testing.T) { tree.OpenAllBranches() t.Run("Branch", func(t *testing.T) { a := getBranch(t, tree, "A") - ar := test.WidgetRenderer(a).(*treeNodeRenderer) + ar := test.TempWidgetRenderer(t, a).(*treeNodeRenderer) a.MouseIn(&desktop.MouseEvent{}) assert.Equal(t, theme.HoverColor(), ar.background.FillColor) assert.True(t, ar.background.Visible()) @@ -926,7 +926,7 @@ func TestTreeNodeRenderer_BackgroundColor_Hovered(t *testing.T) { }) t.Run("Leaf", func(t *testing.T) { b := getLeaf(t, tree, "B") - br := test.WidgetRenderer(b).(*treeNodeRenderer) + br := test.TempWidgetRenderer(t, b).(*treeNodeRenderer) b.MouseIn(&desktop.MouseEvent{}) assert.Equal(t, theme.HoverColor(), br.background.FillColor) assert.True(t, br.background.Visible()) diff --git a/widget/widget.go b/widget/widget.go index c5335485d8..28a32cd5dd 100644 --- a/widget/widget.go +++ b/widget/widget.go @@ -94,9 +94,15 @@ func (w *BaseWidget) Show() { return } - w.SetFieldsAndRefresh(func() { - w.Hidden = false - }) + w.propertyLock.Lock() + w.Hidden = false + w.propertyLock.Unlock() + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() } // Hide this widget so it is no longer visible @@ -136,8 +142,8 @@ func (w *BaseWidget) Refresh() { // // Since: 2.5 func (w *BaseWidget) Theme() fyne.Theme { - w.propertyLock.Lock() - defer w.propertyLock.Unlock() + w.propertyLock.RLock() + defer w.propertyLock.RUnlock() return w.themeWithLock() } @@ -156,23 +162,6 @@ func (w *BaseWidget) themeWithLock() fyne.Theme { return cached } -// SetFieldsAndRefresh helps to make changes to a widget that should be followed by a refresh. -// This method is a guaranteed thread-safe way of directly manipulating widget fields. -// Widgets extending BaseWidget should use this in their setter functions. -// -// Since: 2.5 -func (w *BaseWidget) SetFieldsAndRefresh(f func()) { - w.propertyLock.Lock() - f() - w.propertyLock.Unlock() - - impl := w.super() - if impl == nil { - return - } - impl.Refresh() -} - // super will return the actual object that this represents. // If extended then this is the extending widget, otherwise it is nil. func (w *BaseWidget) super() fyne.Widget { @@ -230,3 +219,18 @@ func (w *DisableableWidget) Disabled() bool { func NewSimpleRenderer(object fyne.CanvasObject) fyne.WidgetRenderer { return internalWidget.NewSimpleRenderer(object) } + +// Orientation controls the horizontal/vertical layout of a widget +type Orientation int + +// Orientation constants to control widget layout +const ( + Horizontal Orientation = 0 + Vertical Orientation = 1 + + // Adaptive will switch between horizontal and vertical layouts according to device orientation. + // This orientation is not always supported and interpretation can vary per-widget. + // + // Since: 2.5 + Adaptive Orientation = 2 +)