Compare commits

...

11 Commits

Author SHA1 Message Date
VewDev
a486cc0694 Merge branch 'allow-change-icon' into 'master'
feat: add ability to change app icon

See merge request [ryubing/ryujinx!128](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/128)
2026-02-19 11:24:36 -06:00
VewDev
1ce1b6f5f2 fix: reload titlebar icon when changing icons
Reload the title bar icon when a new icon is selected in the new "new Ryubing UI" mode.
2025-10-18 09:08:26 +02:00
VewDev
2c727c57bd fix: update translation for app icon instructions 2025-09-04 16:16:25 +02:00
VewDev
e8764a8910 feat: add explanatory tooltip about top left icon reload 2025-09-02 16:56:47 +02:00
VewDev
0bdd4ad091 ui: show icon preview next to name during icon selection 2025-09-02 12:22:33 +02:00
VewDev
8e2f8f4413 feat: implement SVG to PNG conversion for app icon rendering 2025-09-01 15:07:35 +02:00
VewDev
362fbf08a2 refactor: completely overhaul app icon management for cleaner workflow 2025-09-01 14:24:48 +02:00
VewDev
ab4567d0a2 feat: add Bordered Ryugay icon and rename old Ryugay icons to Ryupride 2025-09-01 11:49:16 +02:00
VewDev
1c6f312a27 feat: add new Ryubi app icon 2025-08-29 20:43:05 +02:00
VewDev
8ee675cd62 feat: add fallback to Classic Ryugay for app icon 2025-08-29 20:40:46 +02:00
VewDev
af0f8e2720 feat: add ability to change app icon
Add ability to change application icon to any image file inside the src\Ryujinx\Assets\Icons\AppIcons directory. The app should automatically load any resource in that folder as a selectable icon.
2025-08-29 13:56:28 +02:00
20 changed files with 343 additions and 12 deletions

View File

@@ -12025,6 +12025,56 @@
"zh_TW": "淺色"
}
},
{
"ID": "SettingsTabGeneralIcon",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Application icon:",
"es_ES": "Icono de aplicación:",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabGeneralIconTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "An app restart may be required for the app icon to display properly across Ryujinx.",
"es_ES": "Podría ser necesario reiniciar la aplicación para que el icono se muestre correctamente en todo Ryujinx.",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ControllerSettingsConfigureGeneral",
"Translations": {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 890 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,23 @@
using Avalonia.Media.Imaging;
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.Common.Models
{
public class ApplicationIcon
{
public string Name { get; set; }
public string Filename { get; set; }
public string FullPath
{
get => $"Ryujinx/Assets/Icons/AppIcons/{Filename}";
}
public Bitmap Icon
{
get
{
return RyujinxLogo.GetBitmapForLogo(this);
}
}
}
}

View File

@@ -166,6 +166,7 @@
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
<EmbeddedResource Include="Assets\Icons\AppIcons\*" />
<EmbeddedResource Include="Assets\UIImages\Icon_NCA.png" />
<EmbeddedResource Include="Assets\UIImages\Icon_NRO.png" />
<EmbeddedResource Include="Assets\UIImages\Icon_NSO.png" />

View File

@@ -356,6 +356,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public string BaseStyle { get; set; }
/// <summary>
/// The name of the currently selected window icon
/// </summary>
public string SelectedWindowIcon { get; set; }
/// <summary>
/// Chooses the view mode of the game list // Not Used
/// </summary>

View File

@@ -134,6 +134,7 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.ShownFileTypes.NSO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value;
UI.LanguageCode.Value = shouldLoadFromFile ? cff.LanguageCode : UI.LanguageCode.Value;
UI.BaseStyle.Value = shouldLoadFromFile ? cff.BaseStyle : UI.BaseStyle.Value;
UI.SelectedWindowIcon.Value = shouldLoadFromFile ? cff.SelectedWindowIcon : UI.SelectedWindowIcon.Value;
UI.GameListViewMode.Value = shouldLoadFromFile ? cff.GameListViewMode : UI.GameListViewMode.Value;
UI.ShowNames.Value = shouldLoadFromFile ? cff.ShowNames : UI.ShowNames.Value;
UI.IsAscendingOrder.Value = shouldLoadFromFile ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value;

View File

@@ -151,6 +151,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<string> BaseStyle { get; private set; }
/// <summary>
/// The currently selected window icon.
/// </summary>
public ReactiveObject<string> SelectedWindowIcon { get; private set; }
/// <summary>
/// Start games in fullscreen mode
/// </summary>
@@ -200,6 +205,7 @@ namespace Ryujinx.Ava.Systems.Configuration
ShownFileTypes = new ShownFileTypeSettings();
WindowStartup = new WindowStartupSettings();
BaseStyle = new ReactiveObject<string>();
SelectedWindowIcon = new ReactiveObject<string>();
StartFullscreen = new ReactiveObject<bool>();
StartNoUI = new ReactiveObject<bool>();
GameListViewMode = new ReactiveObject<int>();

View File

@@ -126,6 +126,7 @@ namespace Ryujinx.Ava.Systems.Configuration
},
LanguageCode = UI.LanguageCode,
BaseStyle = UI.BaseStyle,
SelectedWindowIcon = UI.SelectedWindowIcon,
GameListViewMode = UI.GameListViewMode,
ShowNames = UI.ShowNames,
GridSize = UI.GridSize,

View File

@@ -1,28 +1,146 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.ViewModels;
using System.Reflection;
using Ryujinx.Common;
using SkiaSharp;
using Svg.Skia;
using System;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava.UI.Controls
{
public class RyujinxLogo : Image
{
// The UI specifically uses a thicker bordered variant of the icon to avoid crunching out the border at lower resolutions.
// For an example of this, download canary 1.2.95, then open the settings menu, and look at the icon in the top-left.
// The border gets reduced to colored pixels in the 4 corners.
public static readonly Bitmap Bitmap =
new(Assembly.GetAssembly(typeof(MainWindowViewModel))!
.GetManifestResourceStream("Ryujinx.Assets.UIImages.Logo_Ryujinx_AntiAlias.png")!);
public static ReactiveObject<Bitmap> CurrentLogoBitmap { get; private set; } = new();
public RyujinxLogo()
{
Margin = new Thickness(7, 7, 7, 0);
Height = 25;
Width = 25;
Source = Bitmap;
Source = CurrentLogoBitmap.Value;
IsVisible = !ConfigurationState.Instance.ShowOldUI;
ConfigurationState.Instance.UI.SelectedWindowIcon.Event += WindowIconChanged_Event;
CurrentLogoBitmap.Event += CurrentLogoBitmapChanged_Event;
}
private void CurrentLogoBitmapChanged_Event(object _, ReactiveEventArgs<Bitmap> e)
{
Source = e.NewValue;
}
public static void RefreshAppIconFromSettings()
{
SetNewAppIcon(ConfigurationState.Instance.UI.SelectedWindowIcon.Value);
}
private static void SetNewAppIcon(string newIconName)
{
string defaultIconName = "Bordered Ryupride";
if (string.IsNullOrEmpty(newIconName))
{
SetDefaultAppIcon(defaultIconName);
}
ApplicationIcon selectedIcon = RyujinxApp.AvailableApplicationIcons.FirstOrDefault(x => x.Name == newIconName);
if (selectedIcon == null)
{
// Always try to fallback to "Bordered Ryupride" as a default
// If not found, fallback to first found icon
if (newIconName != defaultIconName)
{
SetDefaultAppIcon(defaultIconName);
return;
}
if (RyujinxApp.AvailableApplicationIcons.Count > 0)
{
SetDefaultAppIcon(RyujinxApp.AvailableApplicationIcons.First().Name);
return;
}
}
Stream activeIconStream = EmbeddedResources.GetStream(selectedIcon.FullPath);
if (activeIconStream != null)
{
Bitmap logoBitmap = GetBitmapForLogo(selectedIcon);
if (logoBitmap != null)
{
CurrentLogoBitmap.Value = logoBitmap;
}
}
}
private static void SetDefaultAppIcon(string defaultIconName)
{
// Doing this triggers the WindowIconChanged_Event, which will then
// call SetNewAppIcon again
ConfigurationState.Instance.UI.SelectedWindowIcon.Value = defaultIconName;
}
private void WindowIconChanged_Event(object _, ReactiveEventArgs<string> rArgs) => SetNewAppIcon(rArgs.NewValue);
public static Bitmap GetBitmapForLogo(ApplicationIcon icon)
{
Stream activeIconStream = EmbeddedResources.GetStream(icon.FullPath);
if (activeIconStream == null)
return null;
// SVG files need to be converted to an image first
if (icon.FullPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{
Stream pngStream = ConvertSvgToPng(activeIconStream);
return new Bitmap(pngStream);
}
else
{
return new Bitmap(activeIconStream);
}
}
private static Stream ConvertSvgToPng(Stream svgStream)
{
int width = 256;
int height = 256;
// Load SVG
var svg = new SKSvg();
svg.Load(svgStream);
// Determine size
var picture = svg.Picture;
if (picture == null)
throw new InvalidOperationException("Invalid SVG data");
var picWidth = width > 0 ? width : (int)svg.Picture.CullRect.Width;
var picHeight = height > 0 ? height : (int)svg.Picture.CullRect.Height;
// Create bitmap
using var bitmap = new SKBitmap(picWidth, picHeight);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
// Scale to fit
float scaleX = (float)picWidth / svg.Picture.CullRect.Width;
float scaleY = (float)picHeight / svg.Picture.CullRect.Height;
canvas.Scale(scaleX, scaleY);
canvas.DrawPicture(svg.Picture);
canvas.Flush();
// Encode PNG into memory stream
var outputStream = new MemoryStream();
using (var image = SKImage.FromBitmap(bitmap))
using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
{
data.SaveTo(outputStream);
}
outputStream.Position = 0;
return outputStream;
}
}
}

View File

@@ -8,7 +8,9 @@ using Avalonia.Threading;
using FluentAvalonia.UI.Windowing;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Views.Dialog;
using Ryujinx.Ava.UI.Windows;
@@ -17,7 +19,10 @@ using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava
{
@@ -53,6 +58,9 @@ namespace Ryujinx.Ava
{
Name = FormatTitle();
RetrieveAvailableAppIcons();
RyujinxLogo.RefreshAppIconFromSettings();
AvaloniaXamlLoader.Load(this);
if (OperatingSystem.IsMacOS())
@@ -75,7 +83,6 @@ namespace Ryujinx.Ava
if (Program.PreviewerDetached)
{
ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle);
ConfigurationState.Instance.UI.BaseStyle.Event += ThemeChanged_Event;
}
}
@@ -154,5 +161,27 @@ namespace Ryujinx.Ava
{
await AboutView.Show();
}
public static List<ApplicationIcon> AvailableApplicationIcons { get; set; } = [];
private static void RetrieveAvailableAppIcons()
{
AvailableApplicationIcons.Clear();
string resourceAssemblyPrefix = "Ryujinx.Assets.Icons.AppIcons.";
IEnumerable<string> availableAppIconResources = EmbeddedResources
.GetAllAvailableResources("Ryujinx/Assets")
.Where(x => x.StartsWith(resourceAssemblyPrefix));
foreach (string resource in availableAppIconResources)
{
string filename = resource.Remove(0, resourceAssemblyPrefix.Length);
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
AvailableApplicationIcons.Add(new ApplicationIcon()
{
Name = fileNameWithoutExtension,
Filename = filename
});
}
}
}
}

View File

@@ -10,12 +10,14 @@ using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL3;
using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.System;
using Ryujinx.Ava.Systems.Configuration.UI;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver;
@@ -509,6 +511,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Task.Run(CheckSoundBackends);
Task.Run(PopulateNetworkInterfaces);
ApplicationIcons = new(RyujinxApp.AvailableApplicationIcons);
if (Program.PreviewerDetached)
{
@@ -636,6 +639,7 @@ namespace Ryujinx.Ava.UI.ViewModels
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
FocusLostActionType = (int)config.FocusLostActionType.Value;
AppIconSelectedIndex = _appIcons.ToList().FindIndex(x => x.Name == config.UI.SelectedWindowIcon.Value);
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -754,6 +758,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
config.UI.GameDirs.Value = [.. GameDirectories];
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
config.UI.SelectedWindowIcon.Value = _appIcons[_appIconSelectedIndex].Name;
config.UI.BaseStyle.Value = BaseStyleIndex switch
{
@@ -941,5 +946,27 @@ namespace Ryujinx.Ava.UI.ViewModels
RevertIfNotSaved(IsCustomConfig, IsGameRunning);
CloseWindow?.Invoke();
}
private AvaloniaList<ApplicationIcon> _appIcons = [];
public AvaloniaList<ApplicationIcon> ApplicationIcons
{
get => _appIcons;
set
{
_appIcons = value;
OnPropertyChanged();
}
}
private int _appIconSelectedIndex;
public int AppIconSelectedIndex
{
get => _appIconSelectedIndex;
set
{
_appIconSelectedIndex = value;
OnPropertyChanged();
}
}
}
}

View File

@@ -140,6 +140,32 @@
Content="{ext:Locale SettingsTabGeneralThemeDark}" />
</ComboBox>
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
</StackPanel>
<StackPanel
IsEnabled="{Binding !IsGameTitleNotNull}"
Opacity="{Binding PanelOpacity}"
Margin="0, 15, 0, 10"
ToolTip.Tip="{ext:Locale SettingsTabGeneralIconTooltip}"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralIcon}"
Width="150" />
<ComboBox
MinWidth="100"
HorizontalAlignment="Left"
ItemsSource="{Binding ApplicationIcons}"
SelectedIndex="{Binding AppIconSelectedIndex}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<Image Source="{Binding Icon}" Width="24" Height="24" Margin="0,0,8,0" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
</StackPanel>
</StackPanel>
</StackPanel>

View File

@@ -3,11 +3,13 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using FluentAvalonia.UI.Windowing;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Common;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Windows
@@ -39,7 +41,8 @@ namespace Ryujinx.Ava.UI.Windows
TitleBar.Height = titleBarHeight.Value;
}
Icon = RyujinxLogo.Bitmap;
Icon = RyujinxLogo.CurrentLogoBitmap.Value;
RyujinxLogo.CurrentLogoBitmap.Event += WindowIconChanged_Event;
}
private void LocaleChanged()
@@ -53,6 +56,12 @@ namespace Ryujinx.Ava.UI.Windows
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
}
private void WindowIconChanged_Event(object _, ReactiveEventArgs<Bitmap> rArgs) => UpdateIcon(rArgs.NewValue);
private void UpdateIcon(Bitmap newIcon)
{
Icon = newIcon;
}
}
public abstract class StyleableWindow : Window
@@ -73,7 +82,8 @@ namespace Ryujinx.Ava.UI.Windows
LocaleManager.Instance.LocaleChanged += LocaleChanged;
LocaleChanged();
Icon = new WindowIcon(RyujinxLogo.Bitmap);
Icon = new WindowIcon(RyujinxLogo.CurrentLogoBitmap.Value);
RyujinxLogo.CurrentLogoBitmap.Event += WindowIconChanged_Event;
}
private void LocaleChanged()
@@ -87,5 +97,11 @@ namespace Ryujinx.Ava.UI.Windows
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
}
private void WindowIconChanged_Event(object _, ReactiveEventArgs<Bitmap> rArgs) => UpdateIcon(rArgs.NewValue);
private void UpdateIcon(Bitmap newIcon)
{
Icon = new WindowIcon(newIcon);
}
}
}