1
1
use iced:: highlighter;
2
2
use iced:: time:: { self , milliseconds} ;
3
3
use iced:: widget:: {
4
- self , hover, markdown, right, row, scrollable, text_editor, toggler,
4
+ self , center_x, horizontal_space, hover, image, markdown, pop, right, row,
5
+ scrollable, text_editor, toggler,
5
6
} ;
6
7
use iced:: { Element , Fill , Font , Subscription , Task , Theme } ;
7
8
9
+ use tokio:: task;
10
+
11
+ use std:: collections:: HashMap ;
12
+ use std:: io;
13
+ use std:: sync:: Arc ;
14
+
8
15
pub fn main ( ) -> iced:: Result {
9
16
iced:: application ( "Markdown - Iced" , Markdown :: update, Markdown :: view)
10
17
. subscription ( Markdown :: subscription)
@@ -14,6 +21,7 @@ pub fn main() -> iced::Result {
14
21
15
22
struct Markdown {
16
23
content : text_editor:: Content ,
24
+ images : HashMap < markdown:: Url , Image > ,
17
25
mode : Mode ,
18
26
theme : Theme ,
19
27
}
@@ -26,10 +34,19 @@ enum Mode {
26
34
} ,
27
35
}
28
36
37
+ enum Image {
38
+ Loading ,
39
+ Ready ( image:: Handle ) ,
40
+ #[ allow( dead_code) ]
41
+ Errored ( Error ) ,
42
+ }
43
+
29
44
#[ derive( Debug , Clone ) ]
30
45
enum Message {
31
46
Edit ( text_editor:: Action ) ,
32
47
LinkClicked ( markdown:: Url ) ,
48
+ ImageShown ( markdown:: Url ) ,
49
+ ImageDownloaded ( markdown:: Url , Result < image:: Handle , Error > ) ,
33
50
ToggleStream ( bool ) ,
34
51
NextToken ,
35
52
}
@@ -43,6 +60,7 @@ impl Markdown {
43
60
(
44
61
Self {
45
62
content : text_editor:: Content :: with_text ( INITIAL_CONTENT ) ,
63
+ images : HashMap :: new ( ) ,
46
64
mode : Mode :: Preview ( markdown:: parse ( INITIAL_CONTENT ) . collect ( ) ) ,
47
65
theme,
48
66
} ,
@@ -70,6 +88,25 @@ impl Markdown {
70
88
71
89
Task :: none ( )
72
90
}
91
+ Message :: ImageShown ( url) => {
92
+ if self . images . contains_key ( & url) {
93
+ return Task :: none ( ) ;
94
+ }
95
+
96
+ let _ = self . images . insert ( url. clone ( ) , Image :: Loading ) ;
97
+
98
+ Task :: perform ( download_image ( url. clone ( ) ) , move |result| {
99
+ Message :: ImageDownloaded ( url. clone ( ) , result)
100
+ } )
101
+ }
102
+ Message :: ImageDownloaded ( url, result) => {
103
+ let _ = self . images . insert (
104
+ url,
105
+ result. map ( Image :: Ready ) . unwrap_or_else ( Image :: Errored ) ,
106
+ ) ;
107
+
108
+ Task :: none ( )
109
+ }
73
110
Message :: ToggleStream ( enable_stream) => {
74
111
if enable_stream {
75
112
self . mode = Mode :: Stream {
@@ -126,12 +163,13 @@ impl Markdown {
126
163
Mode :: Stream { parsed, .. } => parsed. items ( ) ,
127
164
} ;
128
165
129
- let preview = markdown (
166
+ let preview = markdown:: view_with (
167
+ & MarkdownViewer {
168
+ images : & self . images ,
169
+ } ,
170
+ & self . theme ,
130
171
items,
131
- markdown:: Settings :: default ( ) ,
132
- markdown:: Style :: from_palette ( self . theme . palette ( ) ) ,
133
- )
134
- . map ( Message :: LinkClicked ) ;
172
+ ) ;
135
173
136
174
row ! [
137
175
editor,
@@ -167,3 +205,92 @@ impl Markdown {
167
205
}
168
206
}
169
207
}
208
+
209
+ struct MarkdownViewer < ' a > {
210
+ images : & ' a HashMap < markdown:: Url , Image > ,
211
+ }
212
+
213
+ impl < ' a > markdown:: Viewer < ' a , Message > for MarkdownViewer < ' a > {
214
+ fn on_link_clicked ( url : markdown:: Url ) -> Message {
215
+ Message :: LinkClicked ( url)
216
+ }
217
+
218
+ fn image (
219
+ & self ,
220
+ _settings : markdown:: Settings ,
221
+ _title : & markdown:: Text ,
222
+ url : & ' a markdown:: Url ,
223
+ ) -> Element < ' a , Message > {
224
+ if let Some ( Image :: Ready ( handle) ) = self . images . get ( url) {
225
+ center_x ( image ( handle) ) . into ( )
226
+ } else {
227
+ pop ( horizontal_space ( ) . width ( 0 ) )
228
+ . key ( url. as_str ( ) )
229
+ . on_show ( |_size| Message :: ImageShown ( url. clone ( ) ) )
230
+ . into ( )
231
+ }
232
+ }
233
+ }
234
+
235
+ async fn download_image ( url : markdown:: Url ) -> Result < image:: Handle , Error > {
236
+ use std:: io;
237
+ use tokio:: task;
238
+
239
+ let client = reqwest:: Client :: new ( ) ;
240
+
241
+ let bytes = client
242
+ . get ( url)
243
+ . send ( )
244
+ . await ?
245
+ . error_for_status ( ) ?
246
+ . bytes ( )
247
+ . await ?;
248
+
249
+ let image = task:: spawn_blocking ( move || {
250
+ Ok :: < _ , Error > (
251
+ :: image:: ImageReader :: new ( io:: Cursor :: new ( bytes) )
252
+ . with_guessed_format ( ) ?
253
+ . decode ( ) ?
254
+ . to_rgba8 ( ) ,
255
+ )
256
+ } )
257
+ . await ??;
258
+
259
+ Ok ( image:: Handle :: from_rgba (
260
+ image. width ( ) ,
261
+ image. height ( ) ,
262
+ image. into_raw ( ) ,
263
+ ) )
264
+ }
265
+
266
+ #[ derive( Debug , Clone ) ]
267
+ pub enum Error {
268
+ RequestFailed ( Arc < reqwest:: Error > ) ,
269
+ IOFailed ( Arc < io:: Error > ) ,
270
+ JoinFailed ( Arc < task:: JoinError > ) ,
271
+ ImageDecodingFailed ( Arc < :: image:: ImageError > ) ,
272
+ }
273
+
274
+ impl From < reqwest:: Error > for Error {
275
+ fn from ( error : reqwest:: Error ) -> Self {
276
+ Self :: RequestFailed ( Arc :: new ( error) )
277
+ }
278
+ }
279
+
280
+ impl From < io:: Error > for Error {
281
+ fn from ( error : io:: Error ) -> Self {
282
+ Self :: IOFailed ( Arc :: new ( error) )
283
+ }
284
+ }
285
+
286
+ impl From < task:: JoinError > for Error {
287
+ fn from ( error : task:: JoinError ) -> Self {
288
+ Self :: JoinFailed ( Arc :: new ( error) )
289
+ }
290
+ }
291
+
292
+ impl From < :: image:: ImageError > for Error {
293
+ fn from ( error : :: image:: ImageError ) -> Self {
294
+ Self :: ImageDecodingFailed ( Arc :: new ( error) )
295
+ }
296
+ }
0 commit comments