fix(text): show selected formatting and style controls

Use category-filtered font and selected-item dropdowns, separate horizontal and vertical alignment, and reuse the border-color palette templates for text color. Persist vertical alignment additively. Follow the established static text-source pattern: https://obsproject.com/kb/text-sources
This commit is contained in:
2026-11-04 17:05:06 -08:00
parent 5c4acf141b
commit 5f7bcc2425
15 changed files with 417 additions and 52 deletions
+131
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using ytLive.Models;
using ytLive.Services;
@@ -7,9 +8,138 @@ namespace ytLive.ViewModels;
public partial class MainViewModel
{
private static readonly string[] SystemTextFontFamilies = Fonts.SystemFontFamilies
.Select(font => font.Source)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(name => name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
private static readonly double[] CommonTextFontSizes =
{ 12, 16, 20, 24, 32, 36, 48, 60, 72, 96, 120, 144, 180, 240, 300 };
private static readonly TextLayerAlignment[] HorizontalAlignmentOptions =
Enum.GetValues<TextLayerAlignment>();
private static readonly TextLayerVerticalAlignment[] VerticalAlignmentOptions =
Enum.GetValues<TextLayerVerticalAlignment>();
private readonly TextOverlayRenderer _textOverlayRenderer = new();
private readonly HashSet<Source> _wiredTextOverlays = new();
private string _selectedTextFontType = "Sans Serif";
public IReadOnlyList<string> TextFontTypeOptions { get; } =
new[] { "Sans Serif", "Serif", "Monospace", "Display", "Other" };
public string SelectedTextFontType
{
get => _selectedTextFontType;
set
{
if (SetProperty(ref _selectedTextFontType, value))
{
OnPropertyChanged(nameof(TextFontFamilies));
OnPropertyChanged(nameof(SelectedTextFontFamily));
}
}
}
public IReadOnlyList<string> TextFontFamilies
{
get
{
var selected = SelectedElement is Source { Type: SourceType.TextOverlay } source
? source.TextFontFamily
: null;
var options = SystemTextFontFamilies
.Where(font => FontTypeFor(font) == SelectedTextFontType)
.ToList();
if (selected != null && FontTypeFor(selected) == SelectedTextFontType
&& !options.Contains(selected, StringComparer.Ordinal))
options.Add(selected);
return options;
}
}
public string? SelectedTextFontFamily
{
get => (SelectedElement as Source)?.TextFontFamily;
set
{
if (SelectedElement is Source { Type: SourceType.TextOverlay } source
&& !string.IsNullOrWhiteSpace(value))
source.TextFontFamily = value;
}
}
public IReadOnlyList<double> TextFontSizeOptions
{
get
{
var selected = (SelectedElement as Source)?.TextFontSize;
return selected is { } size && !CommonTextFontSizes.Contains(size)
? CommonTextFontSizes.Append(size).OrderBy(value => value).ToArray()
: CommonTextFontSizes;
}
}
public IReadOnlyList<TextLayerAlignment> TextHorizontalAlignmentOptions =>
HorizontalAlignmentOptions;
public IReadOnlyList<TextLayerVerticalAlignment> TextVerticalAlignmentOptions =>
VerticalAlignmentOptions;
private void SyncTextFontCategory()
{
SelectedTextFontType = SelectedElement is Source { Type: SourceType.TextOverlay } source
? FontTypeFor(source.TextFontFamily)
: "Sans Serif";
OnPropertyChanged(nameof(SelectedTextFontFamily));
OnPropertyChanged(nameof(TextFontSizeOptions));
}
private static string FontTypeFor(string font)
{
if (font.Contains("Mono", StringComparison.OrdinalIgnoreCase)
|| font.Contains("Consolas", StringComparison.OrdinalIgnoreCase)
|| font.Contains("Courier", StringComparison.OrdinalIgnoreCase)
|| font.Contains("Fixedsys", StringComparison.OrdinalIgnoreCase)
|| font.Contains("Terminal", StringComparison.OrdinalIgnoreCase))
return "Monospace";
if (SerifFonts.Contains(font, StringComparer.OrdinalIgnoreCase)
|| (font.Contains("Serif", StringComparison.OrdinalIgnoreCase)
&& !font.Contains("Sans", StringComparison.OrdinalIgnoreCase)))
return "Serif";
if (font.Contains("Display", StringComparison.OrdinalIgnoreCase)
|| font.Contains("Script", StringComparison.OrdinalIgnoreCase)
|| DisplayFonts.Contains(font, StringComparer.OrdinalIgnoreCase))
return "Display";
if (font.Contains("Sans", StringComparison.OrdinalIgnoreCase)
|| SansSerifFonts.Contains(font, StringComparer.OrdinalIgnoreCase))
return "Sans Serif";
return "Other";
}
private static readonly string[] SerifFonts =
{
"Cambria", "Constantia", "Georgia", "Garamond", "Palatino Linotype", "Times New Roman",
"Book Antiqua", "Century Schoolbook", "Sitka Text", "Sitka Heading", "Bodoni MT", "Bell MT",
};
private static readonly string[] DisplayFonts =
{
"Algerian", "Broadway", "Chiller", "Comic Sans MS", "Cooper Black", "Impact", "Jokerman",
"Magneto", "Papyrus", "Showcard Gothic", "Stencil",
};
private static readonly string[] SansSerifFonts =
{
"Arial", "Arial Nova", "Aptos", "Bahnschrift", "Calibri", "Candara", "Century Gothic",
"Corbel", "Franklin Gothic Medium", "Microsoft Sans Serif", "Segoe UI", "Tahoma",
"Trebuchet MS", "Verdana",
};
private void UpdateConfiguredTextOverlays()
{
var sources = Scenes.SelectMany(scene => scene.Elements).OfType<Source>()
@@ -54,6 +184,7 @@ public partial class MainViewModel
if (propertyName is nameof(Source.TextContent) or nameof(Source.TextFontFamily)
or nameof(Source.TextFontSize) or nameof(Source.TextColor) or nameof(Source.TextBold)
or nameof(Source.TextItalic) or nameof(Source.TextUnderline) or nameof(Source.TextAlignment)
or nameof(Source.TextVerticalAlignment)
or nameof(Source.Width) or nameof(Source.Height))
UpdateTextOverlayPreview(source);
}