1
1
use crate :: desktop_file:: DesktopFiles ;
2
2
use crate :: { arc_mut, lock} ;
3
+ use cfg_if:: cfg_if;
3
4
use color_eyre:: { Help , Report , Result } ;
4
5
use gtk:: cairo:: Surface ;
5
6
use gtk:: gdk_pixbuf:: Pixbuf ;
6
- use gtk:: gio:: { Cancellable , MemoryInputStream } ;
7
7
use gtk:: prelude:: * ;
8
8
use gtk:: { IconLookupFlags , IconTheme , Image } ;
9
9
use std:: cell:: RefCell ;
10
10
use std:: collections:: HashMap ;
11
- use std:: path:: PathBuf ;
12
11
use std:: path:: { Path , PathBuf } ;
13
12
use std:: sync:: { Arc , Mutex } ;
14
13
#[ cfg( feature = "http" ) ]
15
14
use tokio:: sync:: mpsc;
16
15
use tracing:: { debug, trace, warn} ;
17
- use tracing:: { debug, warn} ;
16
+
17
+ cfg_if ! (
18
+ if #[ cfg( feature = "http" ) ] {
19
+ use gtk:: gio:: { Cancellable , MemoryInputStream } ;
20
+ use tracing:: error;
21
+ }
22
+ ) ;
18
23
19
24
#[ derive( Debug , Clone , Eq , Hash , PartialEq ) ]
20
25
struct ImageRef {
@@ -78,44 +83,6 @@ impl Provider {
78
83
}
79
84
}
80
85
81
- /// Attempts to resolve the provided input into a `Pixbuf`,
82
- /// and load that `Pixbuf` into the provided `Image` widget.
83
- ///
84
- /// If `use_fallback` is `true`, a fallback icon will be used
85
- /// where an image cannot be found.
86
- ///
87
- /// Returns `true` if the image was successfully loaded,
88
- /// or `false` if the image could not be found.
89
- /// May also return an error if the resolution or loading process failed.
90
- pub async fn load_into_image (
91
- & self ,
92
- input : & str ,
93
- size : i32 ,
94
- use_fallback : bool ,
95
- image : & Image ,
96
- ) -> Result < bool > {
97
- let image_ref = self . get_ref ( input, size) . await ?;
98
- debug ! ( "image ref for {input}: {:?}" , image_ref) ;
99
-
100
- let pixbuf = if let Some ( pixbuf) = lock ! ( self . cache) . pixbuf_cache . get ( & image_ref) {
101
- pixbuf. clone ( )
102
- } else {
103
- let pixbuf = Self :: get_pixbuf ( & image_ref, image. scale_factor ( ) , use_fallback) . await ?;
104
-
105
- lock ! ( self . cache)
106
- . pixbuf_cache
107
- . insert ( image_ref, pixbuf. clone ( ) ) ;
108
-
109
- pixbuf
110
- } ;
111
-
112
- if let Some ( ref pixbuf) = pixbuf {
113
- create_and_load_surface ( pixbuf, image) ?;
114
- }
115
-
116
- Ok ( pixbuf. is_some ( ) )
117
- }
118
-
119
86
/// Like [`Provider::load_into_image`], but does not return an error if the image could not be found.
120
87
///
121
88
/// If an image is not resolved, a warning is logged. Errors are also logged.
@@ -137,13 +104,13 @@ impl Provider {
137
104
///
138
105
/// This contains the location of the image if it can be resolved.
139
106
/// The ref will be loaded from cache if present.
140
- async fn get_ref ( & self , input : & str , size : i32 ) -> Result < ImageRef > {
107
+ async fn get_ref ( & self , input : & str , use_fallback : bool , size : i32 ) -> Result < ImageRef > {
141
108
let key = ( input. into ( ) , size) ;
142
109
143
110
if let Some ( location) = lock ! ( self . cache) . location_cache . get ( & key) {
144
111
Ok ( location. clone ( ) )
145
112
} else {
146
- let location = self . resolve_location ( input, size, 0 ) . await ?;
113
+ let location = self . resolve_location ( input, size, use_fallback , 0 ) . await ?;
147
114
let image_ref = ImageRef :: new ( size, location, self . icon_theme ( ) ) ;
148
115
149
116
lock ! ( self . cache)
@@ -153,6 +120,17 @@ impl Provider {
153
120
}
154
121
}
155
122
123
+ /// Returns true if the input starts with a prefix
124
+ /// that is supported by the parser
125
+ /// (i.e. the parser would not fall back to checking the input).
126
+ pub fn is_explicit_input ( input : & str ) -> bool {
127
+ input. starts_with ( "icon:" )
128
+ || input. starts_with ( "file://" )
129
+ || input. starts_with ( "http://" )
130
+ || input. starts_with ( "https://" )
131
+ || input. starts_with ( '/' )
132
+ }
133
+
156
134
/// Attempts to resolve the provided input into an `ImageLocation`.
157
135
///
158
136
/// This will resolve all of:
@@ -165,8 +143,19 @@ impl Provider {
165
143
& self ,
166
144
input : & str ,
167
145
size : i32 ,
146
+ use_fallback : bool ,
168
147
recurse_depth : u8 ,
169
148
) -> Result < Option < ImageLocation > > {
149
+ macro_rules! fallback {
150
+ ( ) => {
151
+ if use_fallback {
152
+ Some ( Self :: get_fallback_icon( ) )
153
+ } else {
154
+ None
155
+ }
156
+ } ;
157
+ }
158
+
170
159
const MAX_RECURSE_DEPTH : u8 = 2 ;
171
160
172
161
let input = self . overrides . get ( input) . map_or ( input, String :: as_str) ;
@@ -188,9 +177,11 @@ impl Provider {
188
177
input_name. chars ( ) . skip ( "steam_app_" . len ( ) ) . collect ( ) ,
189
178
) ) ,
190
179
None if self
191
- . icon_theme ( )
192
- . lookup_icon ( input_name, size, IconLookupFlags :: empty ( ) )
193
- . is_some ( ) =>
180
+ . icon_theme
181
+ . borrow ( )
182
+ . as_ref ( )
183
+ . map ( |t| t. has_icon ( input) )
184
+ . unwrap_or ( false ) =>
194
185
{
195
186
Some ( ImageLocation :: Icon ( input_name. to_string ( ) ) )
196
187
}
@@ -200,7 +191,7 @@ impl Provider {
200
191
Report :: msg( format!( "Unsupported image type: {input_type}" ) )
201
192
. note( "You may need to recompile with support if available" )
202
193
) ;
203
- None
194
+ fallback ! ( )
204
195
}
205
196
None if PathBuf :: from ( input_name) . is_file ( ) => {
206
197
Some ( ImageLocation :: Local ( PathBuf :: from ( input_name) ) )
@@ -217,96 +208,104 @@ impl Provider {
217
208
if location == input_name {
218
209
None
219
210
} else {
220
- Box :: pin ( self . resolve_location ( & location, size, recurse_depth + 1 ) ) . await ?
211
+ Box :: pin ( self . resolve_location (
212
+ & location,
213
+ size,
214
+ use_fallback,
215
+ recurse_depth + 1 ,
216
+ ) )
217
+ . await ?
221
218
}
222
219
} else {
223
- None
220
+ warn ! ( "Failed to find image: {input}" ) ;
221
+ fallback ! ( )
224
222
}
225
223
}
226
- None => None ,
224
+ None => {
225
+ warn ! ( "Failed to find image: {input}" ) ;
226
+ fallback ! ( )
227
+ }
227
228
} ;
228
229
229
230
Ok ( location)
230
231
}
231
232
232
- /// Attempts to load the provided `ImageRef` into a `Pixbuf`.
233
- ///
234
- /// If `use_fallback` is `true`, a fallback icon will be used
235
- /// where an image cannot be found.
236
- async fn get_pixbuf (
237
- image_ref : & ImageRef ,
238
- scale : i32 ,
233
+ /// Attempts to fetch the image from the location
234
+ /// and load it into the provided `GTK::Image` widget.
235
+ pub async fn load_into_image (
236
+ & self ,
237
+ input : & str ,
238
+ size : i32 ,
239
239
use_fallback : bool ,
240
- ) -> Result < Option < Pixbuf > > {
241
- const FALLBACK_ICON_NAME : & str = "dialog-question-symbolic" ;
242
-
243
- let buf = match & image_ref. location {
244
- Some ( ImageLocation :: Icon ( name) ) => image_ref. theme . load_icon_for_scale (
245
- name,
246
- image_ref. size ,
247
- scale,
248
- IconLookupFlags :: FORCE_SIZE ,
249
- ) ,
250
- Some ( ImageLocation :: Local ( path) ) => {
251
- let scaled_size = image_ref. size * scale;
252
- Pixbuf :: from_file_at_scale ( path, scaled_size, scaled_size, true ) . map ( Some )
253
- }
254
- Some ( ImageLocation :: Steam ( app_id) ) => {
255
- let path = dirs:: data_dir ( ) . map_or_else (
256
- || Err ( Report :: msg ( "Missing XDG data dir" ) ) ,
257
- |dir| Ok ( dir. join ( format ! ( "icons/hicolor/32x32/apps/steam_icon_{app_id}.png" ) ) ) ,
258
- ) ?;
259
-
260
- let scaled_size = image_ref. size * scale;
261
- Pixbuf :: from_file_at_scale ( path, scaled_size, scaled_size, true ) . map ( Some )
262
- }
263
- #[ cfg( feature = "http" ) ]
264
- Some ( ImageLocation :: Remote ( uri) ) => {
265
- let res = reqwest:: get ( uri. clone ( ) ) . await ?;
240
+ image : & Image ,
241
+ ) -> Result < bool > {
242
+ let image_ref = self . get_ref ( input, use_fallback, size) . await ?;
243
+ let scale = image. scale_factor ( ) ;
244
+ // handle remote locations async to avoid blocking UI thread while downloading
245
+ #[ cfg( feature = "http" ) ]
246
+ if let Some ( ImageLocation :: Remote ( url) ) = & image_ref. location {
247
+ let res = reqwest:: get ( url. clone ( ) ) . await ?;
248
+
249
+ let status = res. status ( ) ;
250
+ let bytes = if status. is_success ( ) {
251
+ let bytes = res. bytes ( ) . await ?;
252
+ Ok ( glib:: Bytes :: from_owned ( bytes) )
253
+ } else {
254
+ Err ( Report :: msg ( format ! (
255
+ "Received non-success HTTP code ({status})"
256
+ ) ) )
257
+ } ?;
258
+
259
+ let stream = MemoryInputStream :: from_bytes ( & bytes) ;
260
+ let scaled_size = image_ref. size * scale;
261
+
262
+ let pixbuf = Pixbuf :: from_stream_at_scale (
263
+ & stream,
264
+ scaled_size,
265
+ scaled_size,
266
+ true ,
267
+ Some ( & Cancellable :: new ( ) ) ,
268
+ )
269
+ . map ( Some ) ?;
270
+
271
+ image. set_from_pixbuf ( pixbuf. as_ref ( ) ) ;
272
+ } else {
273
+ self . load_into_image_sync ( & image_ref, image) ;
274
+ } ;
266
275
267
- let status = res. status ( ) ;
268
- let bytes = if status. is_success ( ) {
269
- let bytes = res. bytes ( ) . await ?;
270
- Ok ( glib:: Bytes :: from_owned ( bytes) )
271
- } else {
272
- Err ( Report :: msg ( format ! (
273
- "Received non-success HTTP code ({status})"
274
- ) ) )
275
- } ?;
276
-
277
- let stream = MemoryInputStream :: from_bytes ( & bytes) ;
278
- let scaled_size = image_ref. size * scale;
279
-
280
- Pixbuf :: from_stream_at_scale (
281
- & stream,
282
- scaled_size,
283
- scaled_size,
284
- true ,
285
- Some ( & Cancellable :: new ( ) ) ,
286
- )
287
- . map ( Some )
288
- }
289
- None if use_fallback => image_ref. theme . load_icon_for_scale (
290
- FALLBACK_ICON_NAME ,
291
- image_ref. size ,
292
- scale,
293
- IconLookupFlags :: empty ( ) ,
294
- ) ,
295
- None => Ok ( None ) ,
296
- } ?;
297
-
298
- Ok ( buf)
276
+ #[ cfg( not( feature = "http" ) ) ]
277
+ self . load_into_image_sync ( & image_ref, image) ;
278
+
279
+ Ok ( true )
299
280
}
300
281
301
- /// Returns true if the input starts with a prefix
302
- /// that is supported by the parser
303
- /// (i.e. the parser would not fall back to checking the input).
304
- pub fn is_explicit_input ( input : & str ) -> bool {
305
- input. starts_with ( "icon:" )
306
- || input. starts_with ( "file://" )
307
- || input. starts_with ( "http://" )
308
- || input. starts_with ( "https://" )
309
- || input. starts_with ( '/' )
282
+ /// Attempts to synchronously fetch an image from location
283
+ /// and load into into the image.
284
+ fn load_into_image_sync ( & self , image_ref : & ImageRef , image : & Image ) {
285
+ let scale = image. scale_factor ( ) ;
286
+
287
+ if let Some ( location) = & image_ref. location {
288
+ match location {
289
+ ImageLocation :: Icon ( name) => image. set_icon_name ( Some ( name) ) ,
290
+ ImageLocation :: Local ( path) => image. set_from_file ( Some ( path) ) ,
291
+ ImageLocation :: Steam ( steam_id) => {
292
+ image. set_from_file ( Self :: steam_id_to_path ( steam_id) . ok ( ) )
293
+ }
294
+ #[ cfg( feature = "http" ) ]
295
+ _ => unreachable ! ( ) , // handled above
296
+ } ;
297
+ }
298
+ }
299
+
300
+ fn steam_id_to_path ( steam_id : & str ) -> Result < PathBuf > {
301
+ dirs:: data_dir ( ) . map_or_else (
302
+ || Err ( Report :: msg ( "Missing XDG data dir" ) ) ,
303
+ |dir| {
304
+ Ok ( dir. join ( format ! (
305
+ "icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
306
+ ) ) )
307
+ } ,
308
+ )
310
309
}
311
310
312
311
pub fn icon_theme ( & self ) -> IconTheme {
@@ -323,31 +322,14 @@ impl Provider {
323
322
324
323
* self . icon_theme . borrow_mut ( ) = if theme. is_some ( ) {
325
324
let icon_theme = IconTheme :: new ( ) ;
326
- icon_theme. set_custom_theme ( theme) ;
325
+ icon_theme. set_theme_name ( theme) ;
327
326
Some ( icon_theme)
328
327
} else {
329
- IconTheme :: default ( )
328
+ Some ( IconTheme :: default ( ) )
330
329
} ;
331
330
}
332
- }
333
331
334
- /// Attempts to create a Cairo `Surface` from the provided `Pixbuf`,
335
- /// using the provided scaling factor.
336
- /// The surface is then loaded into the provided image.
337
- ///
338
- /// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
339
- pub fn create_and_load_surface ( pixbuf : & Pixbuf , image : & Image ) -> Result < ( ) > {
340
- let surface = unsafe {
341
- let ptr = gdk_cairo_surface_create_from_pixbuf (
342
- pixbuf. as_ptr ( ) ,
343
- image. scale_factor ( ) ,
344
- std:: ptr:: null_mut ( ) ,
345
- ) ;
346
-
347
- Surface :: from_raw_full ( ptr)
348
- } ?;
349
-
350
- image. set_from_surface ( Some ( & surface) ) ;
351
-
352
- Ok ( ( ) )
332
+ fn get_fallback_icon ( ) -> ImageLocation {
333
+ ImageLocation :: Icon ( "dialog-question-symbolic" . to_string ( ) )
334
+ }
353
335
}
0 commit comments