1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 15:20:05 -04:00

Add WidgetType::Image and Image::alt_text (#5534)

This adds `WidgetType::Image` and correctly sets it in the Image widget.
This allows us to query for images in kittest tests and tells accesskit
that a node is an image.
It also adds `Image::alt_text` to set a text that will be shown if the
image fails to load and will be read via screen readers. This also
allows us to query images by label in kittest.


* [x] I have followed the instructions in the PR template

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
lucasmerlin
2024-12-30 12:53:46 +01:00
committed by GitHub
parent 86ea3f8a5c
commit e32ca218e8
9 changed files with 96 additions and 12 deletions

View File

@@ -700,6 +700,7 @@ impl WidgetInfo {
WidgetType::DragValue => "drag value",
WidgetType::ColorButton => "color button",
WidgetType::ImageButton => "image button",
WidgetType::Image => "image",
WidgetType::CollapsingHeader => "collapsing header",
WidgetType::ProgressIndicator => "progress indicator",
WidgetType::Window => "window",

View File

@@ -666,6 +666,8 @@ pub enum WidgetType {
ImageButton,
Image,
CollapsingHeader,
ProgressIndicator,

View File

@@ -1017,6 +1017,7 @@ impl Response {
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
Role::Button
}
WidgetType::Image => Role::Image,
WidgetType::Checkbox => Role::CheckBox,
WidgetType::RadioButton => Role::RadioButton,
WidgetType::RadioGroup => Role::RadioGroup,

View File

@@ -344,6 +344,7 @@ impl Widget for Button<'_> {
image_rect,
image.show_loading_spinner,
&image_options,
None,
);
response = widgets::image::texture_load_result_response(
&image.source(ui.ctx()),

View File

@@ -1,12 +1,15 @@
use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
use emath::{Float as _, Rot2};
use epaint::RectShape;
use emath::{Align, Float as _, Rot2};
use epaint::{
text::{LayoutJob, TextFormat, TextWrapping},
RectShape,
};
use crate::{
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2, Align2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape,
Spinner, Stroke, TextStyle, TextureOptions, Ui, Vec2, Widget,
pos2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape, Spinner,
Stroke, TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
};
/// A widget which displays an image.
@@ -51,6 +54,7 @@ pub struct Image<'a> {
sense: Sense,
size: ImageSize,
pub(crate) show_loading_spinner: Option<bool>,
alt_text: Option<String>,
}
impl<'a> Image<'a> {
@@ -76,6 +80,7 @@ impl<'a> Image<'a> {
sense: Sense::hover(),
size,
show_loading_spinner: None,
alt_text: None,
}
}
@@ -255,6 +260,14 @@ impl<'a> Image<'a> {
self.show_loading_spinner = Some(show);
self
}
/// Set alt text for the image. This will be shown when the image fails to load.
/// It will also be read to screen readers.
#[inline]
pub fn alt_text(mut self, label: impl Into<String>) -> Self {
self.alt_text = Some(label.into());
self
}
}
impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
@@ -354,6 +367,7 @@ impl<'a> Image<'a> {
rect,
self.show_loading_spinner,
&self.image_options,
self.alt_text.as_deref(),
);
}
}
@@ -365,6 +379,11 @@ impl<'a> Widget for Image<'a> {
let ui_size = self.calc_size(ui.available_size(), original_image_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::Image);
info.label = self.alt_text.clone();
info
});
if ui.is_rect_visible(rect) {
paint_texture_load_result(
ui,
@@ -372,6 +391,7 @@ impl<'a> Widget for Image<'a> {
rect,
self.show_loading_spinner,
&self.image_options,
self.alt_text.as_deref(),
);
}
texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
@@ -602,6 +622,7 @@ pub fn paint_texture_load_result(
rect: Rect,
show_loading_spinner: Option<bool>,
options: &ImageOptions,
alt: Option<&str>,
) {
match tlr {
Ok(TexturePoll::Ready { texture }) => {
@@ -616,12 +637,28 @@ pub fn paint_texture_load_result(
}
Err(_) => {
let font_id = TextStyle::Body.resolve(ui.style());
ui.painter().text(
rect.center(),
Align2::CENTER_CENTER,
let mut job = LayoutJob {
wrap: TextWrapping::truncate_at_width(rect.width()),
halign: Align::Center,
..Default::default()
};
job.append(
"",
font_id,
ui.visuals().error_fg_color,
0.0,
TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
);
if let Some(alt) = alt {
job.append(
alt,
ui.spacing().item_spacing.x,
TextFormat::simple(font_id, ui.visuals().text_color()),
);
}
let galley = ui.painter().layout_job(job);
ui.painter().galley(
rect.center() - Vec2::Y * galley.size().y * 0.5,
galley,
ui.visuals().text_color(),
);
}
}

View File

@@ -11,6 +11,7 @@ pub struct ImageButton<'a> {
sense: Sense,
frame: bool,
selected: bool,
alt_text: Option<String>,
}
impl<'a> ImageButton<'a> {
@@ -20,6 +21,7 @@ impl<'a> ImageButton<'a> {
sense: Sense::click(),
frame: true,
selected: false,
alt_text: None,
}
}
@@ -87,7 +89,11 @@ impl<'a> Widget for ImageButton<'a> {
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::ImageButton);
info.label = self.alt_text.clone();
info
});
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if self.selected {
@@ -121,7 +127,14 @@ impl<'a> Widget for ImageButton<'a> {
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
let image_options = self.image.image_options().clone();
widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options);
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
None,
&image_options,
self.alt_text.as_deref(),
);
// Draw frame outline:
ui.painter()