1
1
use std:: { borrow:: Cow , slice:: Iter , sync:: Arc , time:: Duration } ;
2
2
3
- use emath:: { Float as _, Rot2 } ;
4
- use epaint:: RectShape ;
3
+ use emath:: { Align , Float as _, Rot2 } ;
4
+ use epaint:: {
5
+ text:: { LayoutJob , TextFormat , TextWrapping } ,
6
+ RectShape ,
7
+ } ;
5
8
6
9
use crate :: {
7
10
load:: { Bytes , SizeHint , SizedTexture , TextureLoadResult , TexturePoll } ,
8
- pos2, Align2 , Color32 , Context , Id , Mesh , Painter , Rect , Response , Rounding , Sense , Shape ,
9
- Spinner , Stroke , TextStyle , TextureOptions , Ui , Vec2 , Widget ,
11
+ pos2, Color32 , Context , Id , Mesh , Painter , Rect , Response , Rounding , Sense , Shape , Spinner ,
12
+ Stroke , TextStyle , TextureOptions , Ui , Vec2 , Widget , WidgetInfo , WidgetType ,
10
13
} ;
11
14
12
15
/// A widget which displays an image.
@@ -51,6 +54,7 @@ pub struct Image<'a> {
51
54
sense : Sense ,
52
55
size : ImageSize ,
53
56
pub ( crate ) show_loading_spinner : Option < bool > ,
57
+ alt_text : Option < String > ,
54
58
}
55
59
56
60
impl < ' a > Image < ' a > {
@@ -76,6 +80,7 @@ impl<'a> Image<'a> {
76
80
sense : Sense :: hover ( ) ,
77
81
size,
78
82
show_loading_spinner : None ,
83
+ alt_text : None ,
79
84
}
80
85
}
81
86
@@ -255,6 +260,14 @@ impl<'a> Image<'a> {
255
260
self . show_loading_spinner = Some ( show) ;
256
261
self
257
262
}
263
+
264
+ /// Set alt text for the image. This will be shown when the image fails to load.
265
+ /// It will also be read to screen readers.
266
+ #[ inline]
267
+ pub fn alt_text ( mut self , label : impl Into < String > ) -> Self {
268
+ self . alt_text = Some ( label. into ( ) ) ;
269
+ self
270
+ }
258
271
}
259
272
260
273
impl < ' a , T : Into < ImageSource < ' a > > > From < T > for Image < ' a > {
@@ -354,6 +367,7 @@ impl<'a> Image<'a> {
354
367
rect,
355
368
self . show_loading_spinner ,
356
369
& self . image_options ,
370
+ self . alt_text . as_deref ( ) ,
357
371
) ;
358
372
}
359
373
}
@@ -365,13 +379,19 @@ impl<'a> Widget for Image<'a> {
365
379
let ui_size = self . calc_size ( ui. available_size ( ) , original_image_size) ;
366
380
367
381
let ( rect, response) = ui. allocate_exact_size ( ui_size, self . sense ) ;
382
+ response. widget_info ( || {
383
+ let mut info = WidgetInfo :: new ( WidgetType :: Image ) ;
384
+ info. label = self . alt_text . clone ( ) ;
385
+ info
386
+ } ) ;
368
387
if ui. is_rect_visible ( rect) {
369
388
paint_texture_load_result (
370
389
ui,
371
390
& tlr,
372
391
rect,
373
392
self . show_loading_spinner ,
374
393
& self . image_options ,
394
+ self . alt_text . as_deref ( ) ,
375
395
) ;
376
396
}
377
397
texture_load_result_response ( & self . source ( ui. ctx ( ) ) , & tlr, response)
@@ -602,6 +622,7 @@ pub fn paint_texture_load_result(
602
622
rect : Rect ,
603
623
show_loading_spinner : Option < bool > ,
604
624
options : & ImageOptions ,
625
+ alt : Option < & str > ,
605
626
) {
606
627
match tlr {
607
628
Ok ( TexturePoll :: Ready { texture } ) => {
@@ -616,12 +637,28 @@ pub fn paint_texture_load_result(
616
637
}
617
638
Err ( _) => {
618
639
let font_id = TextStyle :: Body . resolve ( ui. style ( ) ) ;
619
- ui. painter ( ) . text (
620
- rect. center ( ) ,
621
- Align2 :: CENTER_CENTER ,
640
+ let mut job = LayoutJob {
641
+ wrap : TextWrapping :: truncate_at_width ( rect. width ( ) ) ,
642
+ halign : Align :: Center ,
643
+ ..Default :: default ( )
644
+ } ;
645
+ job. append (
622
646
"⚠" ,
623
- font_id,
624
- ui. visuals ( ) . error_fg_color ,
647
+ 0.0 ,
648
+ TextFormat :: simple ( font_id. clone ( ) , ui. visuals ( ) . error_fg_color ) ,
649
+ ) ;
650
+ if let Some ( alt) = alt {
651
+ job. append (
652
+ alt,
653
+ ui. spacing ( ) . item_spacing . x ,
654
+ TextFormat :: simple ( font_id, ui. visuals ( ) . text_color ( ) ) ,
655
+ ) ;
656
+ }
657
+ let galley = ui. painter ( ) . layout_job ( job) ;
658
+ ui. painter ( ) . galley (
659
+ rect. center ( ) - Vec2 :: Y * galley. size ( ) . y * 0.5 ,
660
+ galley,
661
+ ui. visuals ( ) . text_color ( ) ,
625
662
) ;
626
663
}
627
664
}
0 commit comments