mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
feat(bundler): support Liquid Glass icons, closes #14207
the `icon` config now supports loading an Assets.car directly or a `.icon` (Icon Composer asset) that gets compiled into an Assets.car file
This commit is contained in:
5
.changes/liquid-glass-icon.md
Normal file
5
.changes/liquid-glass-icon.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-bundler": minor:feat
|
||||
---
|
||||
|
||||
Added support to Liquid Glass icons.
|
||||
@@ -23,7 +23,7 @@
|
||||
// files into the `Contents` directory of the bundle.
|
||||
|
||||
use super::{
|
||||
icon::create_icns_file,
|
||||
icon::{app_icon_name_from_assets_car, create_assets_car_file, create_icns_file},
|
||||
sign::{notarize, notarize_auth, notarize_without_stapling, sign, SignTarget},
|
||||
};
|
||||
use crate::{
|
||||
@@ -76,11 +76,19 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let bin_dir = bundle_directory.join("MacOS");
|
||||
let mut sign_paths = Vec::new();
|
||||
|
||||
let bundle_icon_file: Option<PathBuf> =
|
||||
{ create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")? };
|
||||
let bundle_icon_file =
|
||||
create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")?;
|
||||
|
||||
create_info_plist(&bundle_directory, bundle_icon_file, settings)
|
||||
.with_context(|| "Failed to create Info.plist")?;
|
||||
let assets_car_file = create_assets_car_file(&resources_dir, settings)
|
||||
.with_context(|| "Failed to create app Assets.car")?;
|
||||
|
||||
create_info_plist(
|
||||
&bundle_directory,
|
||||
bundle_icon_file,
|
||||
assets_car_file,
|
||||
settings,
|
||||
)
|
||||
.with_context(|| "Failed to create Info.plist")?;
|
||||
|
||||
let framework_paths = copy_frameworks_to_bundle(&bundle_directory, settings)
|
||||
.with_context(|| "Failed to bundle frameworks")?;
|
||||
@@ -204,6 +212,7 @@ fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) ->
|
||||
fn create_info_plist(
|
||||
bundle_dir: &Path,
|
||||
bundle_icon_file: Option<PathBuf>,
|
||||
assets_car_file: Option<PathBuf>,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<()> {
|
||||
let mut plist = plist::Dictionary::new();
|
||||
@@ -213,17 +222,6 @@ fn create_info_plist(
|
||||
"CFBundleExecutable".into(),
|
||||
settings.main_binary_name()?.into(),
|
||||
);
|
||||
if let Some(path) = bundle_icon_file {
|
||||
plist.insert(
|
||||
"CFBundleIconFile".into(),
|
||||
path
|
||||
.file_name()
|
||||
.expect("No file name")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
plist.insert(
|
||||
"CFBundleIdentifier".into(),
|
||||
settings.bundle_identifier().into(),
|
||||
@@ -362,6 +360,27 @@ fn create_info_plist(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(path) = bundle_icon_file {
|
||||
plist.insert(
|
||||
"CFBundleIconFile".into(),
|
||||
path
|
||||
.file_name()
|
||||
.expect("No file name")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(assets_car_file) = assets_car_file {
|
||||
if let Some(icon_name) = app_icon_name_from_assets_car(&assets_car_file) {
|
||||
plist.insert("CFBundleIconName".into(), icon_name.clone().into());
|
||||
plist.insert("CFBundleIconFile".into(), icon_name.into());
|
||||
} else {
|
||||
log::warn!("Failed to get icon name from Assets.car file");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(protocols) = settings.deep_link_protocols() {
|
||||
plist.insert(
|
||||
"CFBundleURLTypes".into(),
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::bundle::Settings;
|
||||
use crate::utils::{self, fs_utils};
|
||||
use crate::utils::{self, fs_utils, CommandExt};
|
||||
use std::{
|
||||
cmp::min,
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use image::GenericImageView;
|
||||
@@ -63,6 +64,11 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Op
|
||||
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
|
||||
if icon_path.extension().map_or(false, |ext| ext == "car") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let icon = image::open(&icon_path)?;
|
||||
let density = if utils::is_retina(&icon_path) { 2 } else { 1 };
|
||||
let (w, h) = icon.dimensions();
|
||||
@@ -113,3 +119,122 @@ fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
|
||||
};
|
||||
icns::Image::from_data(pixel_format, img.width(), img.height(), img.into_bytes())
|
||||
}
|
||||
|
||||
/// Creates an Assets.car file from a .icon file if there are any in the settings.
|
||||
/// Uses an existing Assets.car file if it exists in the settings.
|
||||
/// Returns the path to the Assets.car file.
|
||||
pub fn create_assets_car_file(
|
||||
out_dir: &Path,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
let Some(icons) = settings.bundle_settings().icon.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// If one of the icon files is already a CAR file, just use that.
|
||||
let mut icon_composer_icon_path = None;
|
||||
for icon in icons {
|
||||
let icon_path = Path::new(&icon).to_path_buf();
|
||||
if icon_path.extension() == Some(OsStr::new("car")) {
|
||||
let dest_path = out_dir.join("Assets.car");
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
|
||||
if icon_path.extension() == Some(OsStr::new("icon")) {
|
||||
icon_composer_icon_path.replace(icon_path);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(icon_composer_icon_path) = icon_composer_icon_path else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Create a temporary directory for actool work
|
||||
let temp_dir = tempfile::tempdir()
|
||||
.map_err(|e| crate::Error::GenericError(format!("failed to create temp dir: {e}")))?;
|
||||
|
||||
let icon_dest_path = temp_dir.path().join("Icon.icon");
|
||||
let output_path = temp_dir.path().join("out");
|
||||
|
||||
// Copy the input .icon directory to the temp directory
|
||||
if icon_composer_icon_path.is_dir() {
|
||||
fs_utils::copy_dir(&icon_composer_icon_path, &icon_dest_path)?;
|
||||
} else {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{} must be a directory",
|
||||
icon_composer_icon_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
// Create the output directory
|
||||
fs::create_dir_all(&output_path)?;
|
||||
|
||||
// Run actool command
|
||||
let mut cmd = Command::new("actool");
|
||||
cmd.arg(&icon_dest_path);
|
||||
cmd.arg("--compile");
|
||||
cmd.arg(&output_path);
|
||||
cmd.arg("--output-format");
|
||||
cmd.arg("human-readable-text");
|
||||
cmd.arg("--notices");
|
||||
cmd.arg("--warnings");
|
||||
cmd.arg("--output-partial-info-plist");
|
||||
cmd.arg(output_path.join("assetcatalog_generated_info.plist"));
|
||||
cmd.arg("--app-icon");
|
||||
cmd.arg("Icon");
|
||||
cmd.arg("--include-all-app-icons");
|
||||
cmd.arg("--accent-color");
|
||||
cmd.arg("AccentColor");
|
||||
cmd.arg("--enable-on-demand-resources");
|
||||
cmd.arg("NO");
|
||||
cmd.arg("--development-region");
|
||||
cmd.arg("en");
|
||||
cmd.arg("--target-device");
|
||||
cmd.arg("mac");
|
||||
cmd.arg("--minimum-deployment-target");
|
||||
cmd.arg("26.0");
|
||||
cmd.arg("--platform");
|
||||
cmd.arg("macosx");
|
||||
|
||||
cmd.output_ok()?;
|
||||
|
||||
let assets_car_path = output_path.join("Assets.car");
|
||||
if !assets_car_path.exists() {
|
||||
return Err(crate::Error::GenericError(
|
||||
"actool did not generate Assets.car file".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// copy to out_dir
|
||||
fs_utils::copy_file(&assets_car_path, &out_dir.join("Assets.car"))?;
|
||||
|
||||
Ok(Some(out_dir.join("Assets.car")))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AssetsCarInfo {
|
||||
#[serde(rename = "AssetType", default)]
|
||||
asset_type: String,
|
||||
#[serde(rename = "Name", default)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub fn app_icon_name_from_assets_car(assets_car_path: &Path) -> Option<String> {
|
||||
let Ok(output) = Command::new("assetutil")
|
||||
.arg("--info")
|
||||
.arg(assets_car_path)
|
||||
.output_ok()
|
||||
.inspect_err(|e| log::error!("Failed to get app icon name from Assets.car file: {e}"))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let output = String::from_utf8(output.stdout).ok()?;
|
||||
let assets_car_info: Vec<AssetsCarInfo> = serde_json::from_str(&output)
|
||||
.inspect_err(|e| log::error!("Failed to parse Assets.car file info: {e}"))
|
||||
.ok()?;
|
||||
assets_car_info
|
||||
.iter()
|
||||
.find(|info| info.asset_type == "Icon Image")
|
||||
.map(|info| info.name.clone())
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<
|
||||
// Fall back to non-PNG files for any missing sizes.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("png")) {
|
||||
if icon_path.extension().map_or(false, |ext| ext == "png" || ext == "car") {
|
||||
continue;
|
||||
} else if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;
|
||||
|
||||
@@ -795,6 +795,8 @@ pub struct Settings {
|
||||
local_tools_directory: Option<PathBuf>,
|
||||
/// the bundle settings.
|
||||
bundle_settings: BundleSettings,
|
||||
/// Same as `bundle_settings.icon`, but without the .icon directory.
|
||||
icon_files: Option<Vec<String>>,
|
||||
/// the binaries to bundle.
|
||||
binaries: Vec<BundleBinary>,
|
||||
/// The target platform.
|
||||
@@ -906,6 +908,14 @@ impl SettingsBuilder {
|
||||
};
|
||||
let target_platform = TargetPlatform::from_triple(&target);
|
||||
|
||||
let icon_files = self.bundle_settings.icon.as_ref().map(|paths| {
|
||||
paths
|
||||
.iter()
|
||||
.filter(|p| !p.ends_with(".icon"))
|
||||
.cloned()
|
||||
.collect()
|
||||
});
|
||||
|
||||
Ok(Settings {
|
||||
log_level: self.log_level.unwrap_or(log::Level::Error),
|
||||
package: self
|
||||
@@ -925,6 +935,7 @@ impl SettingsBuilder {
|
||||
.map(|bins| external_binaries(bins, &target, &target_platform)),
|
||||
..self.bundle_settings
|
||||
},
|
||||
icon_files,
|
||||
target_platform,
|
||||
target,
|
||||
no_sign: self.no_sign,
|
||||
@@ -1092,7 +1103,7 @@ impl Settings {
|
||||
|
||||
/// Returns an iterator over the icon files to be used for this bundle.
|
||||
pub fn icon_files(&self) -> ResourcePaths<'_> {
|
||||
match self.bundle_settings.icon {
|
||||
match self.icon_files {
|
||||
Some(ref paths) => ResourcePaths::new(paths.as_slice(), false),
|
||||
None => ResourcePaths::new(&[], false),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user