Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/fyne/internal/commands/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type appData struct {
ResGoString string
Release, rawIcon bool
AdaptiveIcon *metadata.AdaptiveIcon
Splash *metadata.Splash
CustomMetadata map[string]string
Migrations map[string]bool
CanOpen *metadata.CanOpen
Expand Down Expand Up @@ -48,6 +49,7 @@ func (a *appData) mergeMetadata(data *metadata.FyneApp) {
a.AppBuild = data.Details.Build
}
a.AdaptiveIcon = data.AdaptiveIcon
a.Splash = data.Splash

if a.Release {
a.appendCustomMetadata(data.Release)
Expand Down
109 changes: 105 additions & 4 deletions cmd/fyne/internal/commands/package-mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ package commands

import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"fyne.io/fyne/v2"
"fyne.io/tools/cmd/fyne/internal/mobile"
"fyne.io/tools/cmd/fyne/internal/templates"
"fyne.io/tools/cmd/fyne/internal/util"
)

// splashIconSize is the point size of the centred launch screen icon, matching the 288dp
// drawable size of the Android 12+ system splash so the launch looks the same on both platforms.
const splashIconSize = 288

func (p *Packager) packageAndroid(arch string, tags []string) error {
iconFG, iconBG, iconMono := "", "", ""
if p.AdaptiveIcon != nil {
Expand All @@ -23,12 +29,12 @@ func (p *Packager) packageAndroid(arch string, tags []string) error {
}

return mobile.RunNewBuild(arch, p.AppID, p.icon, p.Name, p.AppVersion, p.AppBuild, p.release, p.distribution,
"", "", tags, iconFG, iconBG, iconMono, p.verbose, p.Capabilities)
"", "", tags, iconFG, iconBG, iconMono, p.verbose, p.Capabilities, p.appData.Splash)
}

func (p *Packager) packageIOS(target string, tags []string) error {
err := mobile.RunNewBuild(target, p.AppID, p.icon, p.Name, p.AppVersion, p.AppBuild, p.release, p.distribution,
p.certificate, p.profile, tags, "", "", "", p.verbose, p.Capabilities)
p.certificate, p.profile, tags, "", "", "", p.verbose, p.Capabilities, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -60,14 +66,109 @@ func (p *Packager) packageIOS(target string, tags []string) error {
}
}

if p.appData.Splash != nil {
splashIcon := p.appData.Splash.Icon
if splashIcon == "" {
splashIcon = p.icon
}
if err = writeSplashIconSet(assetDir, splashIcon); err != nil {
return err
}
}

appDir := filepath.Join(p.dir, mobile.AppOutputName(p.os, p.Name, p.release))
if p.verbose {
fmt.Println("Creating icons for", relPath(appDir))
}

return runCmdCaptureOutput("xcrun", "actool", "Images.xcassets", "--compile", appDir, "--platform",
err = runCmdCaptureOutput("xcrun", "actool", "Images.xcassets", "--compile", appDir, "--platform",
"iphoneos", "--target-device", "iphone", "--minimum-deployment-target", "9.0", "--app-icon", "AppIcon",
"--output-format", "human-readable-text", "--output-partial-info-plist", "/dev/null")
if err != nil {
return err
}

if p.appData.Splash != nil {
if err = compileLaunchScreen(p.dir, appDir, p.appData.Splash.Background); err != nil {
return err
}
}

if target == "iossimulator" {
// The build signed the bundle before the resources above were added, and iOS ignores
// a launch storyboard that is not sealed by the signature, so sign it again.
return runCmdCaptureOutput("codesign", "--force", "--sign", "-", appDir)
}
return nil
}

// writeSplashIconSet adds the launch screen icon to the asset catalog as SplashIcon, scaled for 3x displays.
func writeSplashIconSet(assetDir, icon string) error {
imageDir := util.EnsureSubDir(assetDir, "SplashIcon.imageset")
if err := util.WriteScaledPNG(icon, filepath.Join(imageDir, "splash.png"), splashIconSize*3); err != nil {
return fmt.Errorf("failed to write splash icon: %w", err)
}

return os.WriteFile(filepath.Join(imageDir, "Contents.json"), []byte(`{
"images" : [
{
"filename" : "splash.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}`), 0o644)
}

// compileLaunchScreen builds the LaunchScreen storyboard named in Info.plist into the app bundle,
// with the splash background colour and the centred SplashIcon.
func compileLaunchScreen(dir, appDir, background string) error {
r, g, b, err := parseHexColor(background)
if err != nil {
return fmt.Errorf("invalid splash background colour %q: %w", background, err)
}
data := struct {
Red, Green, Blue float64
IconSize int
}{Red: r, Green: g, Blue: b, IconSize: splashIconSize}

storyboard := filepath.Join(dir, "LaunchScreen.storyboard")
out, err := os.Create(storyboard)
if err != nil {
return err
}
defer os.Remove(storyboard)
err = templates.LaunchScreenIOS.Execute(out, data)
_ = out.Close()
if err != nil {
return fmt.Errorf("failed to write launch screen storyboard: %w", err)
}

return runCmdCaptureOutput("xcrun", "ibtool", "--compile", filepath.Join(appDir, "LaunchScreen.storyboardc"),
"--target-device", "iphone", "--minimum-deployment-target", "9.0", storyboard)
}

// parseHexColor converts a "#RRGGBB" colour into 0-1 components, defaulting to white.
func parseHexColor(hex string) (r, g, b float64, err error) {
if hex == "" {
return 1, 1, 1, nil
}
hex = strings.TrimPrefix(hex, "#")
if len(hex) != 6 {
return 0, 0, 0, errors.New("expected #RRGGBB")
}
var channels [3]float64
for i := range channels {
v, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8)
if err != nil {
return 0, 0, 0, err
}
channels[i] = float64(v) / 255
}
return channels[0], channels[1], channels[2], nil
}

func copyResizeIcon(size int, dir, source string) error {
Expand Down
24 changes: 24 additions & 0 deletions cmd/fyne/internal/commands/package-mobile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package commands

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_parseHexColor(t *testing.T) {
r, g, b, err := parseHexColor("#015952")
assert.Nil(t, err)
assert.InDelta(t, 1.0/255, r, 0.001)
assert.InDelta(t, 0x59/255.0, g, 0.001)
assert.InDelta(t, 0x52/255.0, b, 0.001)

r, g, b, err = parseHexColor("")
assert.Nil(t, err)
assert.Equal(t, []float64{1, 1, 1}, []float64{r, g, b})

_, _, _, err = parseHexColor("#0159")
assert.NotNil(t, err)
_, _, _, err = parseHexColor("#01595z")
assert.NotNil(t, err)
}
4 changes: 4 additions & 0 deletions cmd/fyne/internal/commands/package-util-mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ func (m mockUtil) MakePathRelativeTo(root, path string) string {
return utilMakePathRelativeToMock(root, path)
}

func (m mockUtil) WriteScaledPNG(src, dst string, size int) error {
return nil
}

func (m mockUtil) RequireAndroidSDK() error {
return utilRequireAndroidSDKMock()
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/fyne/internal/commands/package-util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type packagerUtil interface {
EnsureSubDir(parent, name string) string
EnsureAbsPath(path string) string
MakePathRelativeTo(root, path string) string
WriteScaledPNG(src, dst string, size int) error

RequireAndroidSDK() error
AndroidBuildToolsPath() string
Expand Down Expand Up @@ -54,6 +55,10 @@ func (d defaultUtil) MakePathRelativeTo(root, path string) string {
return util.MakePathRelativeTo(root, path)
}

func (d defaultUtil) WriteScaledPNG(src, dst string, size int) error {
return util.WriteScaledPNG(src, dst, size)
}

func (d defaultUtil) RequireAndroidSDK() error {
return util.RequireAndroidSDK()
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/fyne/internal/commands/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ func (p *Packager) validate() (err error) {
if data.Details.Icon != "" {
data.Details.Icon = pkgUtil.MakePathRelativeTo(p.srcDir, data.Details.Icon)
}
if data.Splash != nil && data.Splash.Icon != "" {
data.Splash.Icon = util.MakePathRelativeTo(p.srcDir, data.Splash.Icon)
}

p.mergeMetadata(data)
p.sourceMetadata = data.Source
Expand Down
9 changes: 9 additions & 0 deletions cmd/fyne/internal/metadata/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type FyneApp struct {
Description string `toml:",omitempty"`
Details AppDetails
AdaptiveIcon *AdaptiveIcon `toml:",omitempty"`
Splash *Splash `toml:",omitempty"`

Development map[string]string `toml:",omitempty"`
Release map[string]string `toml:",omitempty"`
Expand All @@ -31,6 +32,14 @@ type AdaptiveIcon struct {
Monochrome string `toml:",omitempty"`
}

// Splash describes the native launch screen that iOS and Android show while the app loads.
// Background is a hex colour such as "#015952" and Icon is an optional square PNG that will be centered.
// The icon defaults to the adaptive icon foreground or the app icon.
type Splash struct {
Background string `toml:",omitempty"`
Icon string `toml:",omitempty"`
}

type AppSource struct {
Repo, Dir string `toml:",omitempty"`
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/fyne/internal/metadata/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func TestLoadAppMetadata(t *testing.T) {
assert.NotNil(t, data.Source)
assert.Equal(t, data.Source.Repo, "https://github.com/fyne-io/fyne")
assert.Equal(t, data.Source.Dir, "internal/metadata/testdata")

assert.NotNil(t, data.Splash)
assert.Equal(t, "#015952", data.Splash.Background)
assert.Equal(t, "assets/splash-icon.png", data.Splash.Icon)
}

func TestLoadAppMetadata_Permissions(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions cmd/fyne/internal/metadata/testdata/FyneApp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ ID = "io.fyne.fyne"
Version = "1.0.0"
Build = 1

[Splash]
Background = "#015952"
Icon = "assets/splash-icon.png"

[Development]
InDevelopmentOnly = "Value4"
Test = "Value2"
Expand Down
Loading
Loading