From a0d30759fe432d894a355c0454b710b4037b4bbd Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 29 Dec 2024 13:41:44 +0100 Subject: [PATCH 1/6] Add `WidgetType::Image` --- crates/egui/src/data/output.rs | 1 + crates/egui/src/lib.rs | 2 ++ crates/egui/src/response.rs | 1 + crates/egui/src/widgets/image.rs | 3 ++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index a878bd5fd70..21e54d2cdc3 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -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", diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 1afaada95e1..d89b1c7eb2f 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -664,6 +664,8 @@ pub enum WidgetType { ColorButton, ImageButton, + + Image, CollapsingHeader, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 18ddf793cc6..c65d9ca8c71 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -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, diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 4cdfc5bf749..81fca264971 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -6,7 +6,7 @@ use epaint::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, + Spinner, Stroke, TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType, }; /// A widget which displays an image. @@ -363,6 +363,7 @@ 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(|| WidgetInfo::new(WidgetType::Image)); if ui.is_rect_visible(rect) { paint_texture_load_result( ui, From 05052abf7119d86683fe8194bbcb73f686b4c973 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 29 Dec 2024 14:30:34 +0100 Subject: [PATCH 2/6] Add alt text to Image --- crates/egui/src/widgets/button.rs | 4 +- crates/egui/src/widgets/image.rs | 58 ++++++++++++++++--- crates/egui/src/widgets/image_button.rs | 17 +++++- crates/egui_demo_app/src/apps/image_viewer.rs | 10 ++++ 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index e4355b49e8d..bc411910b14 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -290,7 +290,8 @@ impl Widget for Button<'_> { }); if ui.is_rect_visible(rect) { - let visuals = ui.style().interact(&response); + let style = ui.style().clone(); + let visuals = style.interact(&response); let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected { let selection = ui.visuals().selection; @@ -344,6 +345,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()), diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 81fca264971..205822a69d3 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -1,7 +1,10 @@ use std::{borrow::Cow, sync::Arc, time::Duration}; use emath::{Float as _, Rot2}; -use epaint::RectShape; +use epaint::{ + text::{LayoutJob, TextFormat, TextWrapping}, + RectShape, +}; use crate::{ load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll}, @@ -51,6 +54,7 @@ pub struct Image<'a> { sense: Sense, size: ImageSize, pub(crate) show_loading_spinner: Option, + alt_text: Option, } 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,13 @@ 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. + pub fn alt_text(mut self, label: impl Into) -> Self { + self.alt_text = Some(label.into()); + self + } } impl<'a, T: Into>> From for Image<'a> { @@ -345,13 +357,14 @@ impl<'a> Image<'a> { /// # }); /// ``` #[inline] - pub fn paint_at(&self, ui: &Ui, rect: Rect) { + pub fn paint_at(&self, ui: &mut Ui, rect: Rect) { paint_texture_load_result( ui, &self.load_for_size(ui.ctx(), rect.size()), rect, self.show_loading_spinner, &self.image_options, + self.alt_text.as_deref(), ); } } @@ -363,7 +376,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(|| WidgetInfo::new(WidgetType::Image)); + 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, @@ -371,6 +388,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) @@ -596,11 +614,12 @@ impl<'a> ImageSource<'a> { } pub fn paint_texture_load_result( - ui: &Ui, + ui: &mut Ui, tlr: &TextureLoadResult, rect: Rect, show_loading_spinner: Option, options: &ImageOptions, + alt: Option<&str>, ) { match tlr { Ok(TexturePoll::Ready { texture }) => { @@ -615,12 +634,33 @@ 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::default(); + job.wrap = TextWrapping::wrap_at_width(rect.width()); + 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() + }, + ); + if let Some(alt) = alt { + job.append( + alt, + ui.spacing().item_spacing.x, + TextFormat { + color: ui.visuals().text_color(), + font_id, + ..Default::default() + }, + ); + } + let galley = ui.painter().layout_job(job); + ui.painter().galley( + rect.center() - 0.5 * galley.size(), + galley, + ui.visuals().text_color(), ); } } diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index bcae9a991d5..fdcae898acb 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -11,6 +11,7 @@ pub struct ImageButton<'a> { sense: Sense, frame: bool, selected: bool, + alt_text: Option, } 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() diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index ad7a8448640..a6b2dcef1e5 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -14,6 +14,7 @@ pub struct ImageViewer { fit: ImageFit, maintain_aspect_ratio: bool, max_size: Vec2, + alt_text: String, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -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(), } } } @@ -184,6 +186,11 @@ impl eframe::App for ImageViewer { ui.add_space(5.0); 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() { @@ -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); }); From d43e34d43b300c78e79461ad9f6d6f2a5aa26b3e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 29 Dec 2024 15:07:12 +0100 Subject: [PATCH 3/6] Add snapshot tests for failed images with alt text --- crates/egui/src/widgets/image.rs | 7 ++++--- crates/egui_kittest/tests/regression_tests.rs | 18 +++++++++++++++++- .../tests/snapshots/image_snapshots.png | 3 +++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 crates/egui_kittest/tests/snapshots/image_snapshots.png diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 205822a69d3..cb18752f595 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, sync::Arc, time::Duration}; -use emath::{Float as _, Rot2}; +use emath::{Align, Float as _, Rot2}; use epaint::{ text::{LayoutJob, TextFormat, TextWrapping}, RectShape, @@ -635,7 +635,8 @@ pub fn paint_texture_load_result( Err(_) => { let font_id = TextStyle::Body.resolve(ui.style()); let mut job = LayoutJob::default(); - job.wrap = TextWrapping::wrap_at_width(rect.width()); + job.wrap = TextWrapping::truncate_at_width(rect.width()); + job.halign = Align::Center; job.append( "⚠", 0.0, @@ -658,7 +659,7 @@ pub fn paint_texture_load_result( } let galley = ui.painter().layout_job(job); ui.painter().galley( - rect.center() - 0.5 * galley.size(), + rect.center() - Vec2::Y * galley.size().y * 0.5, galley, ui.visuals().text_color(), ); diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 9493d5443f4..690ca86f6b6 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,4 +1,4 @@ -use egui::Button; +use egui::{Button, Image, Vec2, Widget}; use egui_kittest::{kittest::Queryable, Harness}; #[test] @@ -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"); +} diff --git a/crates/egui_kittest/tests/snapshots/image_snapshots.png b/crates/egui_kittest/tests/snapshots/image_snapshots.png new file mode 100644 index 00000000000..c1b7d6cefc9 --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/image_snapshots.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31faeb4e5f488b8bcee5e090accd326d7e43b264e81768ae7c1907e3b6d0f739 +size 2121 From dfb44f02fc690ee5eef8fc08d6a566d6216f1fea Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 29 Dec 2024 15:10:41 +0100 Subject: [PATCH 4/6] Revert unnecessary changes --- crates/egui/src/lib.rs | 2 +- crates/egui/src/widgets/button.rs | 3 +-- crates/egui/src/widgets/image.rs | 16 +++++++++------- crates/egui_demo_app/src/apps/image_viewer.rs | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index d89b1c7eb2f..954561a8cdb 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -664,7 +664,7 @@ pub enum WidgetType { ColorButton, ImageButton, - + Image, CollapsingHeader, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index bc411910b14..088800e45ce 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -290,8 +290,7 @@ impl Widget for Button<'_> { }); if ui.is_rect_visible(rect) { - let style = ui.style().clone(); - let visuals = style.interact(&response); + let visuals = ui.style().interact(&response); let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected { let selection = ui.visuals().selection; diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index cb18752f595..3b3ab2485bc 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -8,8 +8,8 @@ use epaint::{ 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, WidgetInfo, WidgetType, + 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. @@ -357,7 +357,7 @@ impl<'a> Image<'a> { /// # }); /// ``` #[inline] - pub fn paint_at(&self, ui: &mut Ui, rect: Rect) { + pub fn paint_at(&self, ui: &Ui, rect: Rect) { paint_texture_load_result( ui, &self.load_for_size(ui.ctx(), rect.size()), @@ -614,7 +614,7 @@ impl<'a> ImageSource<'a> { } pub fn paint_texture_load_result( - ui: &mut Ui, + ui: &Ui, tlr: &TextureLoadResult, rect: Rect, show_loading_spinner: Option, @@ -634,9 +634,11 @@ pub fn paint_texture_load_result( } Err(_) => { let font_id = TextStyle::Body.resolve(ui.style()); - let mut job = LayoutJob::default(); - job.wrap = TextWrapping::truncate_at_width(rect.width()); - job.halign = Align::Center; + let mut job = LayoutJob { + wrap: TextWrapping::truncate_at_width(rect.width()), + halign: Align::Center, + ..Default::default() + }; job.append( "⚠", 0.0, diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index a6b2dcef1e5..80961915eb5 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -186,7 +186,7 @@ impl eframe::App for ImageViewer { ui.add_space(5.0); 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"); From 002c0da5d7e65100c01ff8ded288a528251be27c Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 29 Dec 2024 15:59:51 +0100 Subject: [PATCH 5/6] Fix test --- crates/egui/src/widgets/image.rs | 1 + crates/egui_demo_lib/tests/snapshots/widget_gallery.png | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 3b3ab2485bc..5936a240000 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -263,6 +263,7 @@ impl<'a> Image<'a> { /// 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) -> Self { self.alt_text = Some(label.into()); self diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png index 51596247499..6f06e7727f6 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d122b1a995e691b5049c57d65c9f222a5f1639b1e4f6f96f91823444339693cc -size 160540 +oid sha256:b3dc1bf9a59007a6ad0fb66a345d6cf272bd8bdcd26b10dbf411c1280e62b6fc +size 158285 From 3d1db8e26f7a238b98d6ec551f38ffad5a88ffff Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 30 Dec 2024 12:43:53 +0100 Subject: [PATCH 6/6] Apply suggestions from emilk Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/image.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 5936a240000..d08b6e1264e 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -643,21 +643,13 @@ pub fn paint_texture_load_result( job.append( "⚠", 0.0, - TextFormat { - color: ui.visuals().error_fg_color, - font_id: font_id.clone(), - ..Default::default() - }, + TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color), ); if let Some(alt) = alt { job.append( alt, ui.spacing().item_spacing.x, - TextFormat { - color: ui.visuals().text_color(), - font_id, - ..Default::default() - }, + TextFormat::simple(font_id, ui.visuals().text_color()), ); } let galley = ui.painter().layout_job(job);