mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Improve glyph support and embed Segoe Fluent Icons
- Embed SegoeFluentIcons.ttf as an AvaloniaResource - Register SegoeFluentIcons FontFamily in App.axaml using avares URI - Create FontLoader helper for robust font resolution (Embedded > System > File) - Add safety checks to FontLoader to prevent rendering crashes if fonts are invalid - Update ResultViewModel and ResultListBox to use resolved GlyphFontFamily
This commit is contained in:
parent
ed668f3716
commit
29b26643cc
7 changed files with 210 additions and 6 deletions
|
|
@ -14,8 +14,10 @@
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<!-- Additional resources can be added here -->
|
<!-- Embedded fonts -->
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- Register Segoe Fluent Icons font for glyph icons -->
|
||||||
|
<FontFamily x:Key="SegoeFluentIcons">avares://Flow.Launcher.Avalonia/Resources#Segoe Fluent Icons</FontFamily>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
<Content Include="..\Flow.Launcher\Images\*.ico" Link="Images\%(Filename)%(Extension)">
|
<Content Include="..\Flow.Launcher\Images\*.ico" Link="Images\%(Filename)%(Extension)">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<AvaloniaResource Include="Resources\SegoeFluentIcons.ttf" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
195
Flow.Launcher.Avalonia/Helper/FontLoader.cs
Normal file
195
Flow.Launcher.Avalonia/Helper/FontLoader.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for loading fonts from file paths for glyph icons.
|
||||||
|
/// Supports Avalonia's font loading system with custom font files.
|
||||||
|
/// </summary>
|
||||||
|
public static class FontLoader
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, FontFamily> FontFamilyCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a FontFamily from a GlyphInfo, handling file paths and resource paths.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ public partial class MainWindow : Window
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// Get the ViewModel from DI (same instance that App uses)
|
// Get the ViewModel from DI (same instance that App uses)
|
||||||
_viewModel = Ioc.Default.GetRequiredService<MainViewModel>();
|
_viewModel = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||||
_viewModel.HideRequested += () => Hide();
|
_viewModel.HideRequested += () => Hide();
|
||||||
|
|
@ -44,7 +44,7 @@ public partial class MainWindow : Window
|
||||||
protected override void OnLoaded(RoutedEventArgs e)
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnLoaded(e);
|
base.OnLoaded(e);
|
||||||
|
|
||||||
// Focus the query text box when window loads
|
// Focus the query text box when window loads
|
||||||
_queryTextBox?.Focus();
|
_queryTextBox?.Focus();
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +52,10 @@ public partial class MainWindow : Window
|
||||||
protected override void OnOpened(EventArgs e)
|
protected override void OnOpened(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnOpened(e);
|
base.OnOpened(e);
|
||||||
|
|
||||||
// Center the window on screen
|
// Center the window on screen
|
||||||
CenterOnScreen();
|
CenterOnScreen();
|
||||||
|
|
||||||
// Focus and select all text
|
// Focus and select all text
|
||||||
if (_queryTextBox != null)
|
if (_queryTextBox != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
BIN
Flow.Launcher.Avalonia/Resources/SegoeFluentIcons.ttf
Normal file
BIN
Flow.Launcher.Avalonia/Resources/SegoeFluentIcons.ttf
Normal file
Binary file not shown.
|
|
@ -66,6 +66,7 @@ public partial class ResultViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(GlyphAvailable));
|
OnPropertyChanged(nameof(GlyphAvailable));
|
||||||
OnPropertyChanged(nameof(ShowGlyph));
|
OnPropertyChanged(nameof(ShowGlyph));
|
||||||
|
OnPropertyChanged(nameof(GlyphFontFamily));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,4 +75,9 @@ public partial class ResultViewModel : ObservableObject
|
||||||
|
|
||||||
public bool ShowGlyph =>
|
public bool ShowGlyph =>
|
||||||
Settings?.UseGlyphIcons == true && GlyphAvailable;
|
Settings?.UseGlyphIcons == true && GlyphAvailable;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the FontFamily for the glyph icon, handling file paths and resource paths.
|
||||||
|
/// </summary>
|
||||||
|
public FontFamily? GlyphFontFamily => Glyph != null ? FontLoader.GetFontFamily(Glyph) : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
Classes="resultGlyph"
|
Classes="resultGlyph"
|
||||||
Text="{Binding Glyph.Glyph}"
|
Text="{Binding Glyph.Glyph}"
|
||||||
IsVisible="{Binding ShowGlyph}"
|
IsVisible="{Binding ShowGlyph}"
|
||||||
FontFamily="{Binding Glyph.FontFamily}" />
|
FontFamily="{Binding GlyphFontFamily}" />
|
||||||
|
|
||||||
<!-- Image Icon (shown when no glyph or UseGlyphIcons is disabled) -->
|
<!-- Image Icon (shown when no glyph or UseGlyphIcons is disabled) -->
|
||||||
<Image Grid.Column="0"
|
<Image Grid.Column="0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue