Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/egui/src/data/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,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",
Expand Down
2 changes: 2 additions & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ pub enum WidgetType {

ImageButton,

Image,

CollapsingHeader,

ProgressIndicator,
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/egui/src/widgets/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
63 changes: 54 additions & 9 deletions crates/egui/src/widgets/image.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::{borrow::Cow, 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.
Expand Down Expand Up @@ -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> {
Expand All @@ -76,6 +80,7 @@ impl<'a> Image<'a> {
sense: Sense::hover(),
size,
show_loading_spinner: None,
alt_text: None,
}
}

Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -352,6 +365,7 @@ impl<'a> Image<'a> {
rect,
self.show_loading_spinner,
&self.image_options,
self.alt_text.as_deref(),
);
}
}
Expand All @@ -363,13 +377,19 @@ 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,
&tlr,
rect,
self.show_loading_spinner,
&self.image_options,
self.alt_text.as_deref(),
);
}
texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
Expand Down Expand Up @@ -600,6 +620,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 }) => {
Expand All @@ -614,12 +635,36 @@ 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 {
color: ui.visuals().error_fg_color,
font_id: font_id.clone(),
..Default::default()
},
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
);
if let Some(alt) = alt {
job.append(
alt,
ui.spacing().item_spacing.x,
TextFormat {
color: ui.visuals().text_color(),
font_id,
..Default::default()
},
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
);
}
let galley = ui.painter().layout_job(job);
ui.painter().galley(
rect.center() - Vec2::Y * galley.size().y * 0.5,
galley,
ui.visuals().text_color(),
);
}
}
Expand Down
17 changes: 15 additions & 2 deletions crates/egui/src/widgets/image_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct ImageButton<'a> {
sense: Sense,
frame: bool,
selected: bool,
alt_text: Option<String>,
}

impl<'a> ImageButton<'a> {
Expand All @@ -20,6 +21,7 @@ impl<'a> ImageButton<'a> {
sense: Sense::click(),
frame: true,
selected: false,
alt_text: None,
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions crates/egui_demo_app/src/apps/image_viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct ImageViewer {
fit: ImageFit,
maintain_aspect_ratio: bool,
max_size: Vec2,
alt_text: String,
}

#[derive(Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -44,6 +45,7 @@ impl Default for ImageViewer {
fit: ImageFit::Fraction(Vec2::splat(1.0)),
maintain_aspect_ratio: true,
max_size: Vec2::splat(2048.0),
alt_text: "My Image".to_owned(),
}
}
}
Expand Down Expand Up @@ -185,6 +187,11 @@ impl eframe::App for ImageViewer {
ui.label("Aspect ratio is maintained by scaling both sides as necessary");
ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio");

// alt text
ui.add_space(5.0);
ui.label("Alt text");
ui.text_edit_singleline(&mut self.alt_text);

// forget all images
if ui.button("Forget all images").clicked() {
ui.ctx().forget_all_images();
Expand All @@ -211,6 +218,9 @@ impl eframe::App for ImageViewer {
}
image = image.maintain_aspect_ratio(self.maintain_aspect_ratio);
image = image.max_size(self.max_size);
if !self.alt_text.is_empty() {
image = image.alt_text(&self.alt_text);
}

ui.add_sized(ui.available_size(), image);
});
Expand Down
4 changes: 2 additions & 2 deletions crates/egui_demo_lib/tests/snapshots/widget_gallery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion crates/egui_kittest/tests/regression_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use egui::Button;
use egui::{Button, Image, Vec2, Widget};
use egui_kittest::{kittest::Queryable, Harness};

#[test]
Expand Down Expand Up @@ -27,3 +27,19 @@ pub fn focus_should_skip_over_disabled_buttons() {
let button_1 = harness.get_by_label("Button 1");
assert!(button_1.is_focused());
}

#[test]
fn image_failed() {
let mut harness = Harness::new_ui(|ui| {
Image::new("file://invalid/path")
.alt_text("I have an alt text")
.max_size(Vec2::new(100.0, 100.0))
.ui(ui);
});

harness.run();
harness.fit_contents();

#[cfg(all(feature = "wgpu", feature = "snapshot"))]
harness.wgpu_snapshot("image_snapshots");
}
3 changes: 3 additions & 0 deletions crates/egui_kittest/tests/snapshots/image_snapshots.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading