Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/TextForge.Core/Engine/DocumentEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using TextForge.Core.Documents;
using TextForge.Core.Modules;
using TextForge.Core.Templates;
Expand All @@ -7,36 +6,42 @@ 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<TOutput>(Document document, DefaultTemplate template, IRenderTarget<TOutput> target)
public TOutput Render<TOutput>(Document document, IDocumentTemplate? template, IRenderTarget<TOutput> target)
{
ArgumentNullException.ThrowIfNull(target);

var tree = Evaluate(document, template);
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();

Expand Down
4 changes: 2 additions & 2 deletions src/TextForge.Core/Engine/IDocumentEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface IRenderTarget<out TOutput>
/// </summary>
public interface IDocumentEngine
{
RenderTree Evaluate(Document document, DefaultTemplate template);
RenderTree Evaluate(Document document, IDocumentTemplate? template = null);

TOutput Render<TOutput>(Document document, DefaultTemplate template, IRenderTarget<TOutput> target);
TOutput Render<TOutput>(Document document, IDocumentTemplate? template, IRenderTarget<TOutput> target);
}
4 changes: 3 additions & 1 deletion src/TextForge.Core/Templates/DefaultTemblate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";

/// <summary>
/// Global template baseline defaults. Default text color is standard blue.
Expand Down
17 changes: 17 additions & 0 deletions src/TextForge.Core/Templates/IDocumentTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using TextForge.Core.Engine;
using TextForge.Core.Modules;

namespace TextForge.Core.Templates;

/// <summary>
/// Defines the contract for document styling and layout resolution templates.
/// </summary>
public interface IDocumentTemplate
{
string Name { get; }
string DisplayName { get; }
string Description { get; }

ModuleFeatures ResolveFeatures(Module module);
LayoutProperties ResolveLayout(Module module);
}
62 changes: 62 additions & 0 deletions src/TextForge.Core/Templates/TemplateRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace TextForge.Core.Templates;

/// <summary>
/// Central registry providing access to all defined document templates.
/// </summary>
public class TemplateRegistry
{
private readonly Dictionary<string, IDocumentTemplate> _templates = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Global shared registry instance pre-loaded with default templates.
/// </summary>
public static TemplateRegistry Default { get; } = CreateDefault();

/// <summary>
/// Registers a template into the registry.
/// </summary>
public void Register(IDocumentTemplate template)
{
ArgumentNullException.ThrowIfNull(template);
_templates[template.Name] = template;
}

/// <summary>
/// Retrieves a template by its unique name, falling back to "Default" if not found.
/// </summary>
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.");
}

/// <summary>
/// Retrieves all registered templates.
/// </summary>
public IEnumerable<IDocumentTemplate> GetAll() => _templates.Values;

/// <summary>
/// Retrieves all registered template identifiers.
/// </summary>
public IEnumerable<string> GetAvailableNames() => _templates.Values.Select(t => t.Name);

private static TemplateRegistry CreateDefault()
{
var registry = new TemplateRegistry();
registry.Register(new DefaultTemplate());
return registry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public partial class ModuleEditorView : UserControl
public event EventHandler<Module>? ModuleDeleteRequested;
public event EventHandler<Module>? ModuleDuplicateRequested;
public event EventHandler<Module>? ModuleDetachRequested;
public event EventHandler<Module>? ModuleReconnectRequested;
// public event EventHandler<Module>? ModuleReconnectRequested;

public ModuleEditorView()
{
Expand Down
40 changes: 36 additions & 4 deletions src/TextForge.Desktop/Views/Components/PreviewHostView.axaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:templates="using:TextForge.Core.Templates"
x:Class="TextForge.Desktop.Views.Components.PreviewHostView">
<!-- Live Preview Viewport (Uniform A4 Canvas) -->
<Border Padding="20" Background="#E5E7EB">
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Text="Document Preview"
FontWeight="SemiBold"
Margin="0,0,0,8" />
<!-- Header Toolbar: Title & Template Dropdown -->
<Grid DockPanel.Dock="Top" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<TextBlock Text="Document Preview"
FontWeight="SemiBold"
VerticalAlignment="Center" />

<!-- Template Selector Dropdown -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<TextBlock Text="Template:"
FontSize="12"
Foreground="#6B7280"
VerticalAlignment="Center" />

<ComboBox Name="TemplateSelector"
Width="140"
Height="28"
Padding="8,2"
FontSize="12"
ToolTip.Tip="Select Document Template">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="templates:IDocumentTemplate">
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>

<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
Expand Down
62 changes: 59 additions & 3 deletions src/TextForge.Desktop/Views/Components/PreviewHostView.axaml.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Container viewport rendering the compiled Avalonia document canvas preview.
/// </summary>
public partial class PreviewHostView : UserControl
{
private Document? _currentDocument;
private bool _isSyncingSelection;

public event EventHandler<string>? 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);
}
}
}
Loading