22// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
33// See the LICENSE file in the project root for full license information.
44
5+ using System ;
6+ using System . IO ;
7+ using System . Net . Http ;
58using Avalonia ;
69using Avalonia . Controls ;
710using Avalonia . Controls . Documents ;
11+ using Avalonia . Layout ;
812using Avalonia . Media ;
13+ using Avalonia . Media . Imaging ;
14+ using Avalonia . Platform ;
15+ using DocumentsInline = Avalonia . Controls . Documents . Inline ;
16+ using DocumentsInlineUIContainer = Avalonia . Controls . Documents . InlineUIContainer ;
917
1018namespace CrissCross . Avalonia . UI . Controls ;
1119
@@ -17,8 +25,8 @@ public class FormattedTextPresenter : SelectableTextBlock
1725 /// <summary>
1826 /// Property for <see cref="Document"/>.
1927 /// </summary>
20- public static readonly StyledProperty < RichTextDocument ? > DocumentProperty =
21- AvaloniaProperty . Register < FormattedTextPresenter , RichTextDocument ? > ( nameof ( Document ) ) ;
28+ public static readonly StyledProperty < FlowDocument ? > DocumentProperty =
29+ AvaloniaProperty . Register < FormattedTextPresenter , FlowDocument ? > ( nameof ( Document ) ) ;
2230
2331 /// <summary>
2432 /// Property for <see cref="DefaultForeground"/>.
@@ -32,6 +40,8 @@ public class FormattedTextPresenter : SelectableTextBlock
3240 public static readonly StyledProperty < double > DefaultFontSizeProperty =
3341 AvaloniaProperty . Register < FormattedTextPresenter , double > ( nameof ( DefaultFontSize ) , 14 ) ;
3442
43+ private static readonly HttpClient HttpClient = new ( ) ;
44+
3545 /// <summary>
3646 /// Initializes a new instance of the <see cref="FormattedTextPresenter"/> class.
3747 /// </summary>
@@ -44,7 +54,7 @@ public FormattedTextPresenter()
4454 /// <summary>
4555 /// Gets or sets the document to display.
4656 /// </summary>
47- public RichTextDocument ? Document
57+ public FlowDocument ? Document
4858 {
4959 get => GetValue ( DocumentProperty ) ;
5060 set => SetValue ( DocumentProperty , value ) ;
@@ -80,45 +90,57 @@ public void UpdateInlines()
8090 return ;
8191 }
8292
83- Inlines ??= [ ] ;
84-
8593 foreach ( var segment in Document . Segments )
8694 {
87- var run = new Run ( segment . Text )
95+ if ( segment . IsParagraphBreak )
8896 {
89- FontWeight = segment . FontWeight ,
90- FontStyle = segment . FontStyle ,
91- } ;
97+ Inlines ! . Add ( new LineBreak ( ) ) ;
98+ Inlines . Add ( new LineBreak ( ) ) ;
99+ continue ;
100+ }
92101
93- // Apply text decorations
94- if ( segment . TextDecorations is { } decorations )
102+ if ( segment . IsLineBreak )
95103 {
96- run . TextDecorations = decorations ;
104+ Inlines ! . Add ( new LineBreak ( ) ) ;
105+ continue ;
97106 }
98107
99- // Apply custom foreground
100- if ( segment . Foreground is not null )
108+ if ( segment . IsImage )
101109 {
102- run . Foreground = segment . Foreground ;
110+ var imageInline = CreateImageInline ( segment ) ;
111+ if ( imageInline is not null )
112+ {
113+ Inlines ! . Add ( imageInline ) ;
114+ }
115+
116+ continue ;
103117 }
104- else if ( DefaultForeground is not null )
118+
119+ if ( ! segment . HasRenderableText )
105120 {
106- run . Foreground = DefaultForeground ;
121+ continue ;
107122 }
108123
109- // Apply custom font size
110- if ( segment . FontSize . HasValue )
124+ var run = new Run ( segment . Text )
111125 {
112- run . FontSize = segment . FontSize . Value ;
126+ FontWeight = segment . FontWeight ,
127+ FontStyle = segment . FontStyle ,
128+ FontSize = segment . FontSize ?? DefaultFontSize ,
129+ FontFamily = segment . FontFamily ?? FontFamily ,
130+ } ;
131+
132+ if ( segment . TextDecorations is { } decorations )
133+ {
134+ run . TextDecorations = decorations ;
113135 }
114136
115- // Apply custom font family
116- if ( segment . FontFamily is not null )
137+ run . Foreground = segment . Foreground ?? DefaultForeground ?? Foreground ;
138+ if ( segment . Background is not null )
117139 {
118- run . FontFamily = segment . FontFamily ;
140+ run . Background = segment . Background ;
119141 }
120142
121- Inlines . Add ( run ) ;
143+ Inlines ! . Add ( run ) ;
122144 }
123145 }
124146
@@ -142,4 +164,160 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
142164 UpdateInlines ( ) ;
143165 }
144166 }
167+
168+ private static DocumentsInline ? CreateImageInline ( TextSegment segment )
169+ {
170+ if ( string . IsNullOrWhiteSpace ( segment . ImageSource ) )
171+ {
172+ return null ;
173+ }
174+
175+ if ( ! TryLoadImage ( segment . ImageSource , out var bitmap ) )
176+ {
177+ return null ;
178+ }
179+
180+ var image = new Image
181+ {
182+ Source = bitmap ,
183+ Stretch = Stretch . Uniform ,
184+ HorizontalAlignment = segment . ImageAlignment ,
185+ } ;
186+
187+ if ( segment . ImageWidth . HasValue )
188+ {
189+ image . Width = segment . ImageWidth . Value ;
190+ }
191+
192+ if ( segment . ImageHeight . HasValue )
193+ {
194+ image . Height = segment . ImageHeight . Value ;
195+ }
196+
197+ return new DocumentsInlineUIContainer
198+ {
199+ Child = new Border
200+ {
201+ HorizontalAlignment = HorizontalAlignment . Stretch ,
202+ Child = image ,
203+ }
204+ } ;
205+ }
206+
207+ private static bool TryLoadImage ( string source , out IImage ? bitmap )
208+ {
209+ bitmap = null ;
210+
211+ try
212+ {
213+ if ( TryLoadDataUri ( source , out bitmap ) )
214+ {
215+ return true ;
216+ }
217+
218+ if ( Uri . TryCreate ( source , UriKind . Absolute , out var absolute ) )
219+ {
220+ if ( absolute . IsFile )
221+ {
222+ var localPath = absolute . LocalPath ;
223+ if ( File . Exists ( localPath ) )
224+ {
225+ using var fileStream = File . OpenRead ( localPath ) ;
226+ bitmap = CreateBitmapFromStream ( fileStream ) ;
227+ return true ;
228+ }
229+
230+ return false ;
231+ }
232+
233+ if ( absolute . Scheme is "http" or "https" )
234+ {
235+ return TryLoadFromHttp ( absolute , out bitmap ) ;
236+ }
237+
238+ using var assetStream = AssetLoader . Open ( absolute ) ;
239+ bitmap = CreateBitmapFromStream ( assetStream ) ;
240+ return true ;
241+ }
242+
243+ if ( File . Exists ( source ) )
244+ {
245+ using var fallbackFile = File . OpenRead ( source ) ;
246+ bitmap = CreateBitmapFromStream ( fallbackFile ) ;
247+ return true ;
248+ }
249+ }
250+ catch
251+ {
252+ bitmap = null ;
253+ }
254+
255+ return false ;
256+ }
257+
258+ private static bool TryLoadDataUri ( string source , out IImage ? bitmap )
259+ {
260+ bitmap = null ;
261+ if ( ! source . StartsWith ( "data:" , StringComparison . OrdinalIgnoreCase ) )
262+ {
263+ return false ;
264+ }
265+
266+ var commaIndex = source . IndexOf ( ',' ) ;
267+ if ( commaIndex < 0 )
268+ {
269+ return false ;
270+ }
271+
272+ var metadata = source [ ..commaIndex ] ;
273+ if ( ! metadata . Contains ( ";base64" , StringComparison . OrdinalIgnoreCase ) )
274+ {
275+ return false ;
276+ }
277+
278+ var payload = source [ ( commaIndex + 1 ) ..] . Trim ( ) ;
279+ if ( payload . Length == 0 )
280+ {
281+ return false ;
282+ }
283+
284+ try
285+ {
286+ var bytes = Convert . FromBase64String ( payload ) ;
287+ using var memory = new MemoryStream ( bytes ) ;
288+ bitmap = new Bitmap ( memory ) ;
289+ return true ;
290+ }
291+ catch
292+ {
293+ bitmap = null ;
294+ return false ;
295+ }
296+ }
297+
298+ private static bool TryLoadFromHttp ( Uri uri , out IImage ? bitmap )
299+ {
300+ bitmap = null ;
301+ try
302+ {
303+ using var response = HttpClient . GetAsync ( uri , HttpCompletionOption . ResponseHeadersRead ) . GetAwaiter ( ) . GetResult ( ) ;
304+ response . EnsureSuccessStatusCode ( ) ;
305+ using var stream = response . Content . ReadAsStreamAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
306+ bitmap = CreateBitmapFromStream ( stream ) ;
307+ return true ;
308+ }
309+ catch
310+ {
311+ bitmap = null ;
312+ return false ;
313+ }
314+ }
315+
316+ private static Bitmap CreateBitmapFromStream ( Stream stream )
317+ {
318+ using var buffer = new MemoryStream ( ) ;
319+ stream . CopyTo ( buffer ) ;
320+ buffer . Position = 0 ;
321+ return new Bitmap ( buffer ) ;
322+ }
145323}
0 commit comments