VectSharp  2.2.1
A light library for C# vector graphics
MarkdownCanvas.axaml.cs
1 /*
2  VectSharp - A light library for C# vector graphics.
3  Copyright (C) 2020-2022 Giorgio Bianchini
4 
5  This program is free software: you can redistribute it and/or modify
6  it under the terms of the GNU Lesser General Public License as published by
7  the Free Software Foundation, version 3.
8 
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  GNU Lesser General Public License for more details.
13 
14  You should have received a copy of the GNU Lesser General Public License
15  along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17 
18 using Avalonia;
19 using Avalonia.Controls;
20 using Avalonia.Controls.ApplicationLifetimes;
21 using Avalonia.Markup.Xaml;
22 using Avalonia.Threading;
23 using Markdig;
24 using Markdig.Syntax;
25 using System;
26 using System.Collections.Generic;
27 using VectSharp.Canvas;
28 using VectSharp.Markdown;
29 
31 {
32  /// <summary>
33  /// A control to display a Markdown document in an Avalonia application.
34  /// </summary>
35  public class MarkdownCanvasControl : UserControl
36  {
37  /// <summary>
38  /// Defines the <see cref="MaxRenderWidth"/> property.
39  /// </summary>
40  public static readonly StyledProperty<double> MaxRenderWidthProperty = AvaloniaProperty.Register<MarkdownCanvasControl, double>(nameof(MaxRenderWidth), double.PositiveInfinity);
41 
42  /// <summary>
43  /// The maximum width for the rendered document. This will be used even if the control's client area is larger than this (the alignment of the document within the controll will depend on the control's <see cref="ContentControl.HorizontalContentAlignment"/>).
44  /// </summary>
45  public double MaxRenderWidth
46  {
47  get { return GetValue(MaxRenderWidthProperty); }
48  set { SetValue(MaxRenderWidthProperty, value); }
49  }
50 
51  /// <summary>
52  /// Defines the <see cref="MinRenderWidth"/> property.
53  /// </summary>
54  public static readonly StyledProperty<double> MinRenderWidthProperty = AvaloniaProperty.Register<MarkdownCanvasControl, double>(nameof(MinRenderWidth), 200);
55 
56  /// <summary>
57  /// The minimum width for the rendered document. If the control's client area is smaller than this, the horizontal scroll bar will be activated.
58  /// </summary>
59  public double MinRenderWidth
60  {
61  get { return GetValue(MinRenderWidthProperty); }
62  set { SetValue(MinRenderWidthProperty, value); }
63  }
64 
65  /// <summary>
66  /// Defines the <see cref="MinVariation"/> property.
67  /// </summary>
68  public static readonly StyledProperty<double> MinVariationProperty = AvaloniaProperty.Register<MarkdownCanvasControl, double>(nameof(MinVariation), 10);
69 
70  /// <summary>
71  /// The minimum width variation that triggers a document reflow. If the control is resized, but the width changes by less than this amount, the document is not re-drawn.
72  /// </summary>
73  public double MinVariation
74  {
75  get { return GetValue(MinVariationProperty); }
76  set { SetValue(MinVariationProperty, value); }
77  }
78 
79  /// <summary>
80  /// Defines the <see cref="DocumentSource"/> property.
81  /// </summary>
82  public static readonly StyledProperty<string> DocumentSourceProperty = AvaloniaProperty.Register<MarkdownCanvasControl, string>(nameof(DocumentSource));
83 
84  /// <summary>
85  /// Sets the currently displayed document from Markdown source.
86  /// </summary>
87  public string DocumentSource
88  {
89  set { SetValue(DocumentSourceProperty, value); }
90  }
91 
92  /// <summary>
93  /// Defines the <see cref="Document"/> property.
94  /// </summary>
95  public static readonly StyledProperty<MarkdownDocument> DocumentProperty = AvaloniaProperty.Register<MarkdownCanvasControl, MarkdownDocument>(nameof(Document));
96 
97  /// <summary>
98  /// Gets or sets the currently displayed <see cref="MarkdownDocument"/>.
99  /// </summary>
100  public MarkdownDocument Document
101  {
102  get { return GetValue(DocumentProperty); }
103  set { SetValue(DocumentProperty, value); }
104  }
105 
106  /// <summary>
107  /// Defines the <see cref="TextConversionOption"/> property.
108  /// </summary>
110 
111  /// <summary>
112  /// Gets or sets the value that determines whether text items should be converted into paths when drawing.
113  /// Setting this to <see cref="AvaloniaContextInterpreter.TextOptions.NeverConvert"/> will improve performance if you are using custom fonts, but may cause unexpected results unless the font families being used are of type <see cref="ResourceFontFamily"/>.
114  /// </summary>
116  {
117  get { return GetValue(TextConversionOptionsProperty); }
118  set { SetValue(TextConversionOptionsProperty, value); }
119  }
120 
121  /// <summary>
122  /// The <see cref="MarkdownRenderer"/> used to render the <see cref="Document"/>. You can use the properties of this object to customise the rendering. Note that setting the <see cref="Avalonia.Controls.Primitives.TemplatedControl.FontSize"/> of the <see cref="MarkdownCanvasControl"/> will propagate to the <see cref="Renderer"/>'s <see cref="MarkdownRenderer.BaseFontSize"/>.
123  /// </summary>
124  public MarkdownRenderer Renderer { get; }
125 
126  private double lastRenderedWidth = double.NaN;
127 
128  private bool initialized = false;
129 
130  /// <summary>
131  /// Initialises a new <see cref="MarkdownCanvasControl"/>.
132  /// </summary>
134  {
135  InitializeComponent();
136  this.Renderer = new MarkdownRenderer() { BaseFontSize = this.FontSize, Margins = new Margins(10, 10, 10, 10), ImageUriResolver = ImageCache.ImageUriResolver };
137  this.initialized = true;
138  ImageCache.SetExitEventHandler();
139  }
140 
141  /// <inheritdoc/>
142  protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
143  {
144  base.OnPropertyChanged(change);
145 
146  if (this.initialized)
147  {
148  if (change.Property == MarkdownCanvasControl.BoundsProperty || change.Property == MarkdownCanvasControl.MinRenderWidthProperty || change.Property == MarkdownCanvasControl.MaxRenderWidthProperty || change.Property == MarkdownCanvasControl.MinVariationProperty)
149  {
150  Render();
151  }
152  else if (change.Property == MarkdownCanvasControl.DocumentProperty)
153  {
154  forcedRerender = true;
155  Render();
156  }
157  else if (change.Property == MarkdownCanvasControl.FontSizeProperty)
158  {
159  this.Renderer.BaseFontSize = this.FontSize;
160  forcedRerender = true;
161  Render();
162  }
163  else if (change.Property == MarkdownCanvasControl.DocumentSourceProperty)
164  {
165  MarkdownDocument document = Markdig.Markdown.Parse(change.NewValue.GetValueOrDefault<string>(), new MarkdownPipelineBuilder().UseGridTables().UsePipeTables().UseEmphasisExtras().UseGenericAttributes().UseAutoIdentifiers().UseAutoLinks().UseTaskLists().UseListExtras().UseCitations().UseMathematics().UseSmartyPants().Build());
166  this.Document = document;
167  }
168  else if (change.Property == MarkdownCanvasControl.VerticalContentAlignmentProperty)
169  {
170  this.FindControl<ScrollViewer>("ScrollViewer").VerticalContentAlignment = this.VerticalContentAlignment;
171  }
172  else if (change.Property == MarkdownCanvasControl.HorizontalContentAlignmentProperty)
173  {
174  this.FindControl<ScrollViewer>("ScrollViewer").HorizontalContentAlignment = this.HorizontalContentAlignment;
175  }
176  }
177  }
178 
179  private bool forcedRerender = false;
180 
181  private void Render()
182  {
183  if (Document != null)
184  {
185  double width = Math.Min(MaxRenderWidth, Math.Max(MinRenderWidth, this.Bounds.Width - MinVariation - 13));
186 
187  if (forcedRerender || double.IsNaN(lastRenderedWidth) || width != lastRenderedWidth && width < lastRenderedWidth - MinVariation || width > lastRenderedWidth + MinVariation)
188  {
189  Page pag;
190  Dictionary<string, string> linkDestinations;
191 
192  try
193  {
194  pag = Renderer.RenderSinglePage(this.Document, width, out linkDestinations);
195  }
196  catch
197  {
198  pag = new Page(width, 0);
199  linkDestinations = new Dictionary<string, string>();
200  }
201 
202  Dictionary<string, Delegate> taggedActions = new Dictionary<string, Delegate>();
203  Dictionary<string, Avalonia.Point> linkDestinationPoints = new Dictionary<string, Avalonia.Point>();
204 
205  foreach (KeyValuePair<string, string> linkDestination in linkDestinations)
206  {
207  string url = linkDestination.Value;
208 
209  taggedActions.Add(linkDestination.Key, (Func<RenderAction, IEnumerable<RenderAction>>)(act =>
210  {
211  act.PointerEnter += (s, e) =>
212  {
213  act.Parent.Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand);
214  };
215 
216  act.PointerLeave += (s, e) =>
217  {
218  act.Parent.Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Arrow);
219  };
220 
221  act.PointerPressed += (s, e) =>
222  {
223  if (url.StartsWith("#"))
224  {
225  if (linkDestinationPoints.TryGetValue(url.Substring(1), out Avalonia.Point target))
226  {
227  ScrollViewer scrollViewer = this.FindControl<ScrollViewer>("ScrollViewer");
228 
229  scrollViewer.Offset = new Vector(Math.Max(Math.Min(scrollViewer.Offset.X, target.X), target.X - scrollViewer.Viewport.Width), target.Y);
230  }
231  }
232  else
233  {
234  System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() { FileName = url, UseShellExecute = true });
235  }
236  };
237 
238  if (act.ActionType == RenderAction.ActionTypes.Path)
239  {
240  linkDestinationPoints[linkDestination.Key] = act.Geometry.Bounds.TopLeft.Transform(act.Transform);
241  }
242  else if (act.ActionType == RenderAction.ActionTypes.Text)
243  {
244  linkDestinationPoints[linkDestination.Key] = act.Text.Bounds.TopLeft.Transform(act.Transform);
245  }
246  else if (act.ActionType == RenderAction.ActionTypes.RasterImage)
247  {
248  linkDestinationPoints[linkDestination.Key] = act.ImageDestination.Value.TopLeft.Transform(act.Transform);
249  }
250 
251  return new RenderAction[] { act };
252  }));
253 
254  if (url.StartsWith("#"))
255  {
256  if (!taggedActions.ContainsKey(url.Substring(1)))
257  {
258  taggedActions.Add(url.Substring(1), (Func<RenderAction, IEnumerable<RenderAction>>)(act =>
259  {
260  if (act.ActionType == RenderAction.ActionTypes.Path)
261  {
262  linkDestinationPoints[url.Substring(1)] = act.Geometry.Bounds.TopLeft.Transform(act.Transform);
263  }
264  else if (act.ActionType == RenderAction.ActionTypes.Text)
265  {
266  linkDestinationPoints[url.Substring(1)] = act.Text.Bounds.TopLeft.Transform(act.Transform);
267  }
268  else if (act.ActionType == RenderAction.ActionTypes.RasterImage)
269  {
270  linkDestinationPoints[url.Substring(1)] = act.ImageDestination.Value.TopLeft.Transform(act.Transform);
271  }
272 
273  return new RenderAction[] { act };
274  }));
275  }
276  }
277  }
278 
279  Avalonia.Controls.Canvas can = pag.PaintToCanvas(false, taggedActions, false, this.TextConversionOption);
280 
281  this.FindControl<ScrollViewer>("ScrollViewer").Content = can;
282  this.FindControl<ScrollViewer>("ScrollViewer").Padding = new Thickness(0, 0, 0, 0);
283  lastRenderedWidth = width;
284  forcedRerender = false;
285  }
286  else
287  {
288  this.FindControl<ScrollViewer>("ScrollViewer").Padding = new Thickness(0, 0, width - lastRenderedWidth, 0);
289  }
290  }
291  }
292 
293  private void InitializeComponent()
294  {
295  AvaloniaXamlLoader.Load(this);
296  }
297  }
298 }
VectSharp.MarkdownCanvas.MarkdownCanvasControl.Renderer
MarkdownRenderer Renderer
The MarkdownRenderer used to render the Document. You can use the properties of this object to custom...
Definition: MarkdownCanvas.axaml.cs:124
VectSharp.Canvas
Definition: AvaloniaContext.cs:29
VectSharp.Canvas.AvaloniaContextInterpreter
Contains methods to render a Page to an Avalonia.Controls.Canvas.
Definition: AvaloniaContext.cs:2493
VectSharp.Markdown.MarkdownRenderer.BaseFontSize
double BaseFontSize
The base font size to use when rendering the document. This will be the size of regular elements,...
Definition: MarkdownRenderer.cs:40
VectSharp.Markdown.MarkdownRenderer
Renders Markdown documents into VectSharp graphics objects.
Definition: MarkdownRenderer.cs:36
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MinVariation
double MinVariation
The minimum width variation that triggers a document reflow. If the control is resized,...
Definition: MarkdownCanvas.axaml.cs:74
VectSharp.Canvas.RenderAction
Represents a light-weight rendering action.
Definition: AvaloniaContext.cs:1291
VectSharp
Definition: Brush.cs:26
VectSharp.Document
Represents a collection of pages.
Definition: Document.cs:28
VectSharp.MarkdownCanvas.MarkdownCanvasControl
A control to display a Markdown document in an Avalonia application.
Definition: MarkdownCanvas.axaml.cs:36
VectSharp.MarkdownCanvas.MarkdownCanvasControl.Document
MarkdownDocument Document
Gets or sets the currently displayed MarkdownDocument.
Definition: MarkdownCanvas.axaml.cs:101
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MaxRenderWidthProperty
static readonly StyledProperty< double > MaxRenderWidthProperty
Defines the MaxRenderWidth property.
Definition: MarkdownCanvas.axaml.cs:40
VectSharp.MarkdownCanvas.MarkdownCanvasControl.DocumentSourceProperty
static readonly StyledProperty< string > DocumentSourceProperty
Defines the DocumentSource property.
Definition: MarkdownCanvas.axaml.cs:82
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MaxRenderWidth
double MaxRenderWidth
The maximum width for the rendered document. This will be used even if the control's client area is l...
Definition: MarkdownCanvas.axaml.cs:46
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MinRenderWidth
double MinRenderWidth
The minimum width for the rendered document. If the control's client area is smaller than this,...
Definition: MarkdownCanvas.axaml.cs:60
VectSharp.Markdown.MarkdownRenderer.RenderSinglePage
Page RenderSinglePage(string markdownSource, double width, out Dictionary< string, string > linkDestinations)
Parses the supplied markdownSource using all the supported extensions and renders the resulting docu...
Definition: MarkdownRenderer.cs:409
VectSharp.Canvas.RenderAction.ActionTypes
ActionTypes
Types of rendering actions.
Definition: AvaloniaContext.cs:1296
VectSharp.Markdown
Definition: HtmlTag.cs:26
VectSharp.MarkdownCanvas.MarkdownCanvasControl.DocumentProperty
static readonly StyledProperty< MarkdownDocument > DocumentProperty
Defines the Document property.
Definition: MarkdownCanvas.axaml.cs:95
VectSharp.Markdown.Margins
Represents the margins of a page.
Definition: MarkdownContext.cs:186
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MarkdownCanvasControl
MarkdownCanvasControl()
Initialises a new MarkdownCanvasControl.
Definition: MarkdownCanvas.axaml.cs:133
VectSharp.Canvas.AvaloniaContextInterpreter.TextOptions
TextOptions
Defines whether text items should be converted into paths when drawing.
Definition: AvaloniaContext.cs:2498
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MinRenderWidthProperty
static readonly StyledProperty< double > MinRenderWidthProperty
Defines the MinRenderWidth property.
Definition: MarkdownCanvas.axaml.cs:54
VectSharp.MarkdownCanvas
Definition: ImageCache.cs:24
VectSharp.MarkdownCanvas.MarkdownCanvasControl.TextConversionOptionsProperty
static readonly StyledProperty< AvaloniaContextInterpreter.TextOptions > TextConversionOptionsProperty
Defines the TextConversionOption property.
Definition: MarkdownCanvas.axaml.cs:109
VectSharp.MarkdownCanvas.MarkdownCanvasControl.MinVariationProperty
static readonly StyledProperty< double > MinVariationProperty
Defines the MinVariation property.
Definition: MarkdownCanvas.axaml.cs:68
VectSharp.MarkdownCanvas.MarkdownCanvasControl.DocumentSource
string DocumentSource
Sets the currently displayed document from Markdown source.
Definition: MarkdownCanvas.axaml.cs:88
VectSharp.MarkdownCanvas.MarkdownCanvasControl.TextConversionOption
AvaloniaContextInterpreter.TextOptions TextConversionOption
Gets or sets the value that determines whether text items should be converted into paths when drawing...
Definition: MarkdownCanvas.axaml.cs:116