From ded909a9fba774815c0bc4391bc7701a65642368 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Fri, 28 Aug 2026 06:16:58 +0300 Subject: [PATCH] feat(templates,ui): add TemplateRegistry and dynamic template selector dropdown in preview host --- src/TextForge.Core/Engine/DocumentEngine.cs | 23 ++++--- src/TextForge.Core/Engine/IDocumentEngine.cs | 4 +- .../Templates/DefaultTemblate.cs | 4 +- .../Templates/IDocumentTemplate.cs | 17 +++++ .../Templates/TemplateRegistry.cs | 62 +++++++++++++++++++ .../Components/ModuleEditorView.axaml.cs | 2 +- .../Views/Components/PreviewHostView.axaml | 40 ++++++++++-- .../Views/Components/PreviewHostView.axaml.cs | 62 ++++++++++++++++++- 8 files changed, 194 insertions(+), 20 deletions(-) create mode 100644 src/TextForge.Core/Templates/IDocumentTemplate.cs create mode 100644 src/TextForge.Core/Templates/TemplateRegistry.cs diff --git a/src/TextForge.Core/Engine/DocumentEngine.cs b/src/TextForge.Core/Engine/DocumentEngine.cs index 98b0328..90ff095 100644 --- a/src/TextForge.Core/Engine/DocumentEngine.cs +++ b/src/TextForge.Core/Engine/DocumentEngine.cs @@ -1,4 +1,3 @@ -using System.Linq; using TextForge.Core.Documents; using TextForge.Core.Modules; using TextForge.Core.Templates; @@ -7,22 +6,28 @@ namespace TextForge.Core.Engine; public class DocumentEngine : IDocumentEngine { - private readonly DefaultTemplate _defaultTemplate = new(); + private readonly TemplateRegistry _registry; - public RenderTree Evaluate(Document document, DefaultTemplate template) + public DocumentEngine(TemplateRegistry? registry = null) + { + _registry = registry ?? TemplateRegistry.Default; + } + + public RenderTree Evaluate(Document document, IDocumentTemplate? template = null) { ArgumentNullException.ThrowIfNull(document); - ArgumentNullException.ThrowIfNull(template); + + var activeTemplate = template ?? _registry.Get(document.TemplateName); var rootNodes = document.Modules - .Where(module => module.ConnectionState == ModuleConnectionState.Connected) // Skip detached root modules - .Select(module => EvaluateModule(module, template)) + .Where(module => module.ConnectionState == ModuleConnectionState.Connected) + .Select(module => EvaluateModule(module, activeTemplate)) .ToList(); return new RenderTree(document.Metadata, rootNodes); } - public TOutput Render(Document document, DefaultTemplate template, IRenderTarget target) + public TOutput Render(Document document, IDocumentTemplate? template, IRenderTarget target) { ArgumentNullException.ThrowIfNull(target); @@ -30,13 +35,13 @@ public TOutput Render(Document document, DefaultTemplate template, IRen return target.Render(tree); } - private RenderNode EvaluateModule(Module module, DefaultTemplate template) + private RenderNode EvaluateModule(Module module, IDocumentTemplate template) { var resolvedFeatures = template.ResolveFeatures(module); var resolvedLayout = template.ResolveLayout(module); var children = module.SubModules - .Where(child => child.ConnectionState == ModuleConnectionState.Connected) // Skip detached child modules + .Where(child => child.ConnectionState == ModuleConnectionState.Connected) .Select(child => EvaluateModule(child, template)) .ToList(); diff --git a/src/TextForge.Core/Engine/IDocumentEngine.cs b/src/TextForge.Core/Engine/IDocumentEngine.cs index 152bd11..8b9d450 100644 --- a/src/TextForge.Core/Engine/IDocumentEngine.cs +++ b/src/TextForge.Core/Engine/IDocumentEngine.cs @@ -16,7 +16,7 @@ public interface IRenderTarget /// public interface IDocumentEngine { - RenderTree Evaluate(Document document, DefaultTemplate template); + RenderTree Evaluate(Document document, IDocumentTemplate? template = null); - TOutput Render(Document document, DefaultTemplate template, IRenderTarget target); + TOutput Render(Document document, IDocumentTemplate? template, IRenderTarget target); } diff --git a/src/TextForge.Core/Templates/DefaultTemblate.cs b/src/TextForge.Core/Templates/DefaultTemblate.cs index 917be84..b32052c 100644 --- a/src/TextForge.Core/Templates/DefaultTemblate.cs +++ b/src/TextForge.Core/Templates/DefaultTemblate.cs @@ -5,9 +5,11 @@ namespace TextForge.Core.Templates; -public class DefaultTemplate +public class DefaultTemplate : IDocumentTemplate { public string Name { get; init; } = "Default"; + public string DisplayName => "Default"; + public string Description => "Standard document layout and styling."; /// /// Global template baseline defaults. Default text color is standard blue. diff --git a/src/TextForge.Core/Templates/IDocumentTemplate.cs b/src/TextForge.Core/Templates/IDocumentTemplate.cs new file mode 100644 index 0000000..19b33b0 --- /dev/null +++ b/src/TextForge.Core/Templates/IDocumentTemplate.cs @@ -0,0 +1,17 @@ +using TextForge.Core.Engine; +using TextForge.Core.Modules; + +namespace TextForge.Core.Templates; + +/// +/// Defines the contract for document styling and layout resolution templates. +/// +public interface IDocumentTemplate +{ + string Name { get; } + string DisplayName { get; } + string Description { get; } + + ModuleFeatures ResolveFeatures(Module module); + LayoutProperties ResolveLayout(Module module); +} diff --git a/src/TextForge.Core/Templates/TemplateRegistry.cs b/src/TextForge.Core/Templates/TemplateRegistry.cs new file mode 100644 index 0000000..a2352bd --- /dev/null +++ b/src/TextForge.Core/Templates/TemplateRegistry.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TextForge.Core.Templates; + +/// +/// Central registry providing access to all defined document templates. +/// +public class TemplateRegistry +{ + private readonly Dictionary _templates = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Global shared registry instance pre-loaded with default templates. + /// + public static TemplateRegistry Default { get; } = CreateDefault(); + + /// + /// Registers a template into the registry. + /// + public void Register(IDocumentTemplate template) + { + ArgumentNullException.ThrowIfNull(template); + _templates[template.Name] = template; + } + + /// + /// Retrieves a template by its unique name, falling back to "Default" if not found. + /// + public IDocumentTemplate Get(string name) + { + if (!string.IsNullOrWhiteSpace(name) && _templates.TryGetValue(name, out var template)) + { + return template; + } + + if (_templates.TryGetValue("Default", out var defaultTemplate)) + { + return defaultTemplate; + } + + throw new InvalidOperationException($"Template '{name}' was not found and no Default template is registered."); + } + + /// + /// Retrieves all registered templates. + /// + public IEnumerable GetAll() => _templates.Values; + + /// + /// Retrieves all registered template identifiers. + /// + public IEnumerable GetAvailableNames() => _templates.Values.Select(t => t.Name); + + private static TemplateRegistry CreateDefault() + { + var registry = new TemplateRegistry(); + registry.Register(new DefaultTemplate()); + return registry; + } +} diff --git a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs index 2a8eefc..101bd6a 100644 --- a/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs +++ b/src/TextForge.Desktop/Views/Components/ModuleEditorView.axaml.cs @@ -15,7 +15,7 @@ public partial class ModuleEditorView : UserControl public event EventHandler? ModuleDeleteRequested; public event EventHandler? ModuleDuplicateRequested; public event EventHandler? ModuleDetachRequested; - public event EventHandler? ModuleReconnectRequested; + // public event EventHandler? ModuleReconnectRequested; public ModuleEditorView() { diff --git a/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml b/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml index aa4012d..3806e19 100644 --- a/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml +++ b/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml @@ -1,13 +1,45 @@ - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml.cs b/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml.cs index 4ab9e89..e8bacee 100644 --- a/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml.cs +++ b/src/TextForge.Desktop/Views/Components/PreviewHostView.axaml.cs @@ -1,14 +1,70 @@ +using System; +using System.Linq; using Avalonia.Controls; +using TextForge.Core.Documents; +using TextForge.Core.Templates; namespace TextForge.Desktop.Views.Components; -/// -/// Container viewport rendering the compiled Avalonia document canvas preview. -/// public partial class PreviewHostView : UserControl { + private Document? _currentDocument; + private bool _isSyncingSelection; + + public event EventHandler? TemplateChanged; + public PreviewHostView() { InitializeComponent(); + InitializeTemplateSelector(); + } + + private void InitializeTemplateSelector() + { + // 1. Populate items directly from Core TemplateRegistry + var templates = TemplateRegistry.Default.GetAll().ToList(); + TemplateSelector.ItemsSource = templates; + + // 2. Default to the primary template + TemplateSelector.SelectedItem = templates.FirstOrDefault(); + + // 3. Hook event + TemplateSelector.SelectionChanged += TemplateSelector_SelectionChanged; + } + + public void BindDocument(Document document) + { + _currentDocument = document; + + _isSyncingSelection = true; + try + { + var matchingTemplate = TemplateRegistry.Default + .GetAll() + .FirstOrDefault(t => string.Equals(t.Name, document.TemplateName, StringComparison.OrdinalIgnoreCase)) + ?? TemplateRegistry.Default.Get("Default"); + + TemplateSelector.SelectedItem = matchingTemplate; + } + finally + { + _isSyncingSelection = false; + } + } + + private void TemplateSelector_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (_isSyncingSelection) return; + + if (TemplateSelector.SelectedItem is IDocumentTemplate selectedTemplate) + { + if (_currentDocument is not null && _currentDocument.TemplateName != selectedTemplate.Name) + { + _currentDocument.TemplateName = selectedTemplate.Name; + _currentDocument.NotifyChanged(); + } + + TemplateChanged?.Invoke(this, selectedTemplate.Name); + } } }