diff --git a/Flow.Launcher.Avalonia/App.axaml b/Flow.Launcher.Avalonia/App.axaml index 7a48015b4..a43fc0b4f 100644 --- a/Flow.Launcher.Avalonia/App.axaml +++ b/Flow.Launcher.Avalonia/App.axaml @@ -14,8 +14,10 @@ - + + + avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons diff --git a/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj b/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj index f79c53587..19dc2acbb 100644 --- a/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj +++ b/Flow.Launcher.Avalonia/Flow.Launcher.Avalonia.csproj @@ -66,6 +66,7 @@ PreserveNewest + diff --git a/Flow.Launcher.Avalonia/Helper/FontLoader.cs b/Flow.Launcher.Avalonia/Helper/FontLoader.cs new file mode 100644 index 000000000..b7bb59538 --- /dev/null +++ b/Flow.Launcher.Avalonia/Helper/FontLoader.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using Avalonia; +using Avalonia.Media; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Avalonia.Helper; + +/// +/// Helper for loading fonts from file paths for glyph icons. +/// Supports Avalonia's font loading system with custom font files. +/// +public static class FontLoader +{ + private static readonly ConcurrentDictionary FontFamilyCache = new(); + + /// + /// Get a FontFamily from a GlyphInfo, handling file paths and resource paths. + /// + public static FontFamily? GetFontFamily(GlyphInfo glyph) + { + if (glyph == null || string.IsNullOrEmpty(glyph.FontFamily)) + return null; + + var fontFamilyPath = glyph.FontFamily; + + if (FontFamilyCache.TryGetValue(fontFamilyPath, out var cached)) + return cached; + + FontFamily? result = null; + + // 1. Try as embedded resource font + result = TryGetEmbeddedFont(fontFamilyPath); + + // 2. Try as system font (Avalonia handles this by name) + if (result == null) + result = TryGetSystemFont(fontFamilyPath); + + // 3. Try as file path + if (result == null && IsFilePath(fontFamilyPath)) + result = LoadFontFromFile(fontFamilyPath); + + if (result != null) + FontFamilyCache[fontFamilyPath] = result; + + return result; + } + + private static FontFamily? TryGetEmbeddedFont(string fontFamilyPath) + { + try + { + var fontName = ExtractFontName(fontFamilyPath); + if (string.IsNullOrEmpty(fontName)) + return null; + + // Check for Segoe Fluent Icons specifically (common for Flow Launcher plugins) + if (fontName.Contains("Segoe Fluent Icons", StringComparison.OrdinalIgnoreCase)) + { + // Try to get from Application Resources first + if (Application.Current != null && Application.Current.TryGetResource("SegoeFluentIcons", null, out var resource)) + { + if (resource is FontFamily family) + return family; + } + + // Fallback to direct URI - Folder based is usually better in Avalonia 11 + return SafeCreateFontFamily("avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons"); + } + + return null; + } + catch + { + return null; + } + } + + private static FontFamily? TryGetSystemFont(string fontNameOrPath) + { + try + { + var fontName = ExtractFontName(fontNameOrPath); + if (string.IsNullOrEmpty(fontName)) + return null; + + return SafeCreateFontFamily(fontName); + } + catch + { + return null; + } + } + + private static FontFamily? SafeCreateFontFamily(string nameOrUri) + { + try + { + var family = new FontFamily(nameOrUri); + + // Validate if font actually exists in the system by trying to get a glyph typeface + // This prevents returning a dummy FontFamily that will crash during rendering + if (FontManager.Current.TryGetGlyphTypeface(new Typeface(family), out _)) + { + return family; + } + } + catch + { + // Ignore + } + + return null; + } + + private static bool IsFilePath(string path) + { + return path.StartsWith("file:///", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("file://", StringComparison.OrdinalIgnoreCase) || + (path.Length > 2 && path[1] == ':' && (path[2] == '\\' || path[2] == '/')); + } + + private static string? ExtractFontName(string path) + { + if (string.IsNullOrEmpty(path)) return null; + + // If it's a file path or URL, try to extract the fragment (after #) + var hashIndex = path.IndexOf('#'); + if (hashIndex >= 0 && hashIndex < path.Length - 1) + { + return path.Substring(hashIndex + 1); + } + + // If it contains slashes or backslashes, it might be a path without a fragment + if (path.Contains('/') || path.Contains('\\')) + { + // Try to get the file name without extension as a fallback name + try + { + var fileName = Path.GetFileNameWithoutExtension(path); + if (!string.IsNullOrEmpty(fileName)) + return fileName; + } + catch + { + // Ignore + } + } + + // If it's not a path-like string, assume it's just the font name + return path; + } + + private static FontFamily? LoadFontFromFile(string fontFamilyPath) + { + try + { + var filePath = fontFamilyPath; + if (filePath.StartsWith("file:///", StringComparison.OrdinalIgnoreCase)) + filePath = filePath.Substring(8); + else if (filePath.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + filePath = filePath.Substring(7); + + var hashIndex = filePath.IndexOf('#'); + string fontFilePath; + string? fontName; + + if (hashIndex >= 0) + { + fontFilePath = filePath.Substring(0, hashIndex); + fontName = filePath.Substring(hashIndex + 1); + } + else + { + fontFilePath = filePath; + fontName = null; + } + + if (!File.Exists(fontFilePath)) + return null; + + // In Avalonia 11, for local files we should use absolute file URIs + var uriString = $"file:///{fontFilePath.Replace('\\', '/')}"; + if (!string.IsNullOrEmpty(fontName)) + uriString += $"#{fontName}"; + + return SafeCreateFontFamily(uriString); + } + catch + { + return null; + } + } +} diff --git a/Flow.Launcher.Avalonia/MainWindow.axaml.cs b/Flow.Launcher.Avalonia/MainWindow.axaml.cs index ad46551a3..48ccca9c5 100644 --- a/Flow.Launcher.Avalonia/MainWindow.axaml.cs +++ b/Flow.Launcher.Avalonia/MainWindow.axaml.cs @@ -18,7 +18,7 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); - + // Get the ViewModel from DI (same instance that App uses) _viewModel = Ioc.Default.GetRequiredService(); _viewModel.HideRequested += () => Hide(); @@ -44,7 +44,7 @@ public partial class MainWindow : Window protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - + // Focus the query text box when window loads _queryTextBox?.Focus(); } @@ -52,10 +52,10 @@ public partial class MainWindow : Window protected override void OnOpened(EventArgs e) { base.OnOpened(e); - + // Center the window on screen CenterOnScreen(); - + // Focus and select all text if (_queryTextBox != null) { diff --git a/Flow.Launcher.Avalonia/Resources/SegoeFluentIcons.ttf b/Flow.Launcher.Avalonia/Resources/SegoeFluentIcons.ttf new file mode 100644 index 000000000..8f05a4bbc Binary files /dev/null and b/Flow.Launcher.Avalonia/Resources/SegoeFluentIcons.ttf differ diff --git a/Flow.Launcher.Avalonia/ViewModel/ResultViewModel.cs b/Flow.Launcher.Avalonia/ViewModel/ResultViewModel.cs index 76628091b..632ecb18c 100644 --- a/Flow.Launcher.Avalonia/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher.Avalonia/ViewModel/ResultViewModel.cs @@ -66,6 +66,7 @@ public partial class ResultViewModel : ObservableObject { OnPropertyChanged(nameof(GlyphAvailable)); OnPropertyChanged(nameof(ShowGlyph)); + OnPropertyChanged(nameof(GlyphFontFamily)); } } } @@ -74,4 +75,9 @@ public partial class ResultViewModel : ObservableObject public bool ShowGlyph => Settings?.UseGlyphIcons == true && GlyphAvailable; + + /// + /// Gets the FontFamily for the glyph icon, handling file paths and resource paths. + /// + public FontFamily? GlyphFontFamily => Glyph != null ? FontLoader.GetFontFamily(Glyph) : null; } diff --git a/Flow.Launcher.Avalonia/Views/ResultListBox.axaml b/Flow.Launcher.Avalonia/Views/ResultListBox.axaml index 3daddfd13..376a3f31b 100644 --- a/Flow.Launcher.Avalonia/Views/ResultListBox.axaml +++ b/Flow.Launcher.Avalonia/Views/ResultListBox.axaml @@ -42,7 +42,7 @@ Classes="resultGlyph" Text="{Binding Glyph.Glyph}" IsVisible="{Binding ShowGlyph}" - FontFamily="{Binding Glyph.FontFamily}" /> + FontFamily="{Binding GlyphFontFamily}" />