Forge
csharpdeeb25a2
1using System.ComponentModel;
2using Avalonia.Controls;
3using CascadeIDE.Services.MarkdownPreview;
4using CascadeIDE.ViewModels;
5
6namespace CascadeIDE.Views;
7
8public partial class MarkdownPreviewSurfaceView : UserControl
9{
10 private static readonly IMarkdownPreviewRenderer Renderer = new MarkdigMarkdownPreviewRenderer();
11 private INotifyPropertyChanged? _boundVm;
12
13 public MarkdownPreviewSurfaceView()
14 {
15 InitializeComponent();
16 DataContextChanged += OnDataContextChanged;
17 }
18
19 private MarkdownPreviewSurfaceViewModel? ViewModel => DataContext as MarkdownPreviewSurfaceViewModel;
20
21 private void OnDataContextChanged(object? sender, EventArgs e)
22 {
23 if (_boundVm is not null)
24 _boundVm.PropertyChanged -= OnVmPropertyChanged;
25
26 _boundVm = DataContext as INotifyPropertyChanged;
27 if (_boundVm is not null)
28 _boundVm.PropertyChanged += OnVmPropertyChanged;
29
30 UpdateSurface();
31 }
32
33 private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
34 {
35 if (e.PropertyName is nameof(MarkdownPreviewSurfaceViewModel.Payload)
36 or nameof(MarkdownPreviewSurfaceViewModel.StatusText)
37 or nameof(MarkdownPreviewSurfaceViewModel.ErrorMessage)
38 or nameof(MarkdownPreviewSurfaceViewModel.IsBusy))
39 {
40 UpdateSurface();
41 }
42 }
43
44 private void UpdateSurface()
45 {
46 if (this.FindControl<ContentControl>("PreviewHost") is not { } host)
47 return;
48
49 var vm = ViewModel;
50 if (vm?.Payload is not { } payload)
51 {
52 host.Content = BuildPlaceholder(vm);
53 }
54 else
55 {
56 try
57 {
58 var anchors = new MarkdownPreviewAnchorRegistry();
59 var ctx = new MarkdownPreviewRenderContext(
60 payload.SourcePath,
61 vm.TryGetWorkspaceRoot(),
62 url => vm.TryOpenPreviewLink(url, anchors),
63 anchors);
64 host.Content = Renderer.Render(payload, ctx);
65
66 var scrollLine = vm.ConsumePendingScrollLine();
67 if (scrollLine is > 0)
68 anchors.ScrollToLine(scrollLine.Value);
69
70 var scrollFragment = vm.ConsumePendingScrollFragment();
71 if (!string.IsNullOrWhiteSpace(scrollFragment))
72 anchors.ScrollToFragment(scrollFragment);
73 }
74 catch (Exception ex)
75 {
76 host.Content = BuildRenderErrorPlaceholder(vm, ex);
77 }
78 }
79
80 if (this.FindControl<Border>("PreviewStatusBanner") is not { } banner
81 || this.FindControl<TextBlock>("PreviewStatusText") is not { } text)
82 {
83 return;
84 }
85
86 var message = BuildStatusMessage(vm);
87 banner.IsVisible = !string.IsNullOrWhiteSpace(message);
88 text.Text = message ?? "";
89 }
90
91 private static Control BuildPlaceholder(MarkdownPreviewSurfaceViewModel? vm)
92 {
93 return new Border
94 {
95 Padding = new Avalonia.Thickness(16),
96 Child = new TextBlock
97 {
98 Text = string.IsNullOrWhiteSpace(vm?.StatusText)
99 ? "Markdown preview unavailable."
100 : vm!.StatusText,
101 Opacity = 0.8,
102 TextWrapping = Avalonia.Media.TextWrapping.Wrap
103 }
104 };
105 }
106
107 private static Control BuildRenderErrorPlaceholder(MarkdownPreviewSurfaceViewModel? vm, Exception ex)
108 {
109 var panel = new StackPanel
110 {
111 Spacing = 8,
112 Margin = new Avalonia.Thickness(16),
113 Children =
114 {
115 new TextBlock
116 {
117 Text = "Markdown preview failed to render.",
118 FontWeight = Avalonia.Media.FontWeight.SemiBold,
119 TextWrapping = Avalonia.Media.TextWrapping.Wrap
120 },
121 new TextBlock
122 {
123 Text = ex.Message,
124 Opacity = 0.85,
125 TextWrapping = Avalonia.Media.TextWrapping.Wrap
126 }
127 }
128 };
129 if (!string.IsNullOrWhiteSpace(vm?.StatusText))
130 {
131 panel.Children.Add(new TextBlock
132 {
133 Text = vm!.StatusText,
134 Opacity = 0.7,
135 TextWrapping = Avalonia.Media.TextWrapping.Wrap
136 });
137 }
138
139 return new ScrollViewer
140 {
141 VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
142 HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
143 Content = panel
144 };
145 }
146
147 private static string? BuildStatusMessage(MarkdownPreviewSurfaceViewModel? vm)
148 {
149 if (vm is null)
150 return null;
151
152 var parts = new List<string>();
153 if (vm.IsBusy)
154 parts.Add("Refreshing preview...");
155 if (!string.IsNullOrWhiteSpace(vm.ErrorMessage))
156 parts.Add(vm.ErrorMessage);
157 if (!string.IsNullOrWhiteSpace(vm.StatusText))
158 parts.Add(vm.StatusText);
159
160 return parts.Count == 0 ? null : string.Join(" | ", parts);
161 }
162}
163
View only · write via MCP/CIDE