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}" />