egui_material3/
imagelist.rs

1//! # Material Design Image Lists
2//!
3//! This module provides Material Design 3 image list components with comprehensive
4//! image source support and intelligent caching capabilities.
5//!
6//! ## Features
7//!
8//! - **Multiple image sources**: Local files, online URLs, and embedded byte arrays
9//! - **Smart caching**: Downloaded images are cached with proper file extensions
10//! - **Format detection**: Automatic detection of PNG, JPEG, GIF, and WebP formats
11//! - **Performance optimized**: Efficient loading and UI repainting
12//! - **Error handling**: Graceful fallback with visual indicators
13//!
14//! ## Usage
15//!
16//! ### Local Images
17//! ```rust,no_run
18//! use egui_material3::image_list;
19//!
20//! ui.add(image_list()
21//!     .columns(3)
22//!     .item_spacing(8.0)
23//!     .items_from_paths(glob::glob("resources/*.png")?));
24//! ```
25//!
26//! ### Online Images (OnDemand Feature)
27//!
28//! Enable the `ondemand` feature in your `Cargo.toml`:
29//! ```toml
30//! [dependencies]
31//! egui-material3 = { version = "0.0.5", features = ["ondemand"] }
32//! ```
33//!
34//! ```rust,no_run
35//! use egui_material3::image_list;
36//!
37//! ui.add(image_list()
38//!     .columns(4)
39//!     .item_spacing(8.0)
40//!     .items_from_urls(vec![
41//!         "https://example.com/image1.jpg".to_string(),
42//!         "https://example.com/image2.png".to_string(),
43//!     ]));
44//! ```
45//!
46//! ### Embedded Images
47//! ```rust,no_run
48//! use egui_material3::image_list;
49//!
50//! ui.add(image_list()
51//!     .columns(2)
52//!     .item_spacing(8.0)
53//!     .items_from_bytes(vec![
54//!         include_bytes!("image1.png").to_vec(),
55//!         include_bytes!("image2.png").to_vec(),
56//!     ]));
57//! ```
58//!
59//! ## OnDemand Feature Details
60//!
61//! When the `ondemand` feature is enabled, the image list provides:
62//!
63//! - **Automatic downloading**: Images are downloaded from URLs on first access
64//! - **Smart caching**: Downloaded images are saved to `/tmp/egui_material3_img/` with proper extensions
65//! - **Format detection**: File extensions are determined from content (PNG, JPEG, GIF, WebP)
66//! - **Efficient reuse**: Cached images are reused without re-downloading
67//! - **Performance optimization**: UI only repaints when new images are available
68//! - **Error handling**: Failed downloads show visual indicators instead of crashing
69//!
70//! ### Cache Management
71//!
72//! - Cache directory: `/tmp/egui_material3_img/`
73//! - File naming: `img_{hash}.{extension}` (e.g., `img_abc123.png`)
74//! - Automatic cleanup: Cache persists between runs for efficiency
75//! - Manual cleanup: Remove `/tmp/egui_material3_img/` to clear cache
76
77use crate::theme::get_global_color;
78use egui::{
79    ecolor::Color32,
80    epaint::{Stroke, CornerRadius},
81    Rect, Response, Sense, Ui, Vec2, Widget
82};
83use std::env;
84use std::io::Read;
85use std::hash::{Hash, Hasher};
86use image::GenericImageView;
87
88/// Material Design image list variants.
89#[derive(Clone, Copy, Debug, PartialEq)]
90pub enum ImageListVariant {
91    Standard,
92    Masonry,
93    Woven,
94}
95
96/// Material Design image list component.
97///
98/// Image lists display a collection of images in an organized grid.
99/// They're commonly used to display a collection of photos or other images.
100///
101/// ```
102/// # egui::__run_test_ui(|ui| {
103/// let image_list = MaterialImageList::standard()
104///     .columns(3)
105///     .item("Image 1", "320x240.png")
106///     .item("Image 2", "320x240.png")
107///     .item("Image 3", "320x240.png");
108///
109/// ui.add(image_list);
110/// # });
111/// ```
112#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
113pub struct MaterialImageList<'a> {
114    variant: ImageListVariant,
115    items: Vec<ImageListItem<'a>>,
116    columns: usize,
117    item_spacing: f32,
118    text_protected: bool,
119    corner_radius: CornerRadius,
120    id_salt: Option<String>,
121    tmppath: String,
122}
123
124pub struct ImageListItem<'a> {
125    pub label: String,
126    pub image_source: Option<String>,
127    pub supporting_text: Option<String>,
128    pub on_click: Option<Box<dyn Fn() + Send + Sync>>,
129    _phantom: std::marker::PhantomData<&'a ()>,
130}
131
132impl<'a> ImageListItem<'a> {
133    pub fn new(label: impl Into<String>, image_source: impl Into<String>) -> Self {
134        Self {
135            label: label.into(),
136            image_source: Some(image_source.into()),
137            supporting_text: None,
138            on_click: None,
139            _phantom: std::marker::PhantomData,
140        }
141    }
142    
143    pub fn supporting_text(mut self, text: impl Into<String>) -> Self {
144        self.supporting_text = Some(text.into());
145        self
146    }
147    
148    pub fn on_click<F>(mut self, callback: F) -> Self 
149    where
150        F: Fn() + Send + Sync + 'static,
151    {
152        self.on_click = Some(Box::new(callback));
153        self
154    }
155}
156
157impl<'a> MaterialImageList<'a> {
158    /// Create a new standard image list.
159    pub fn standard() -> Self {
160        Self::new(ImageListVariant::Standard)
161    }
162
163    /// Create a new masonry image list.
164    pub fn masonry() -> Self {
165        Self::new(ImageListVariant::Masonry)
166    }
167
168    /// Create a new woven image list.
169    pub fn woven() -> Self {
170        Self::new(ImageListVariant::Woven)
171    }
172
173    fn new(variant: ImageListVariant) -> Self {
174        // create img folder in tmp dir using let dir = env::temp_dir(), save the path to tmppath on MaterialImageList
175        let tmppath = env::temp_dir().join("egui_material3_img");
176        let _ = std::fs::create_dir_all(&tmppath);
177
178        Self {
179            variant,
180            items: Vec::new(),
181            columns: 3,
182            item_spacing: 8.0,
183            text_protected: false,
184            corner_radius: CornerRadius::from(4.0),
185            id_salt: None,
186            tmppath: tmppath.to_string_lossy().to_string(),
187        }
188    }
189
190    /// Set number of columns.
191    pub fn columns(mut self, columns: usize) -> Self {
192        self.columns = columns.max(1);
193        self
194    }
195
196    /// Add an image item.
197    pub fn item(mut self, label: impl Into<String>, image_source: impl Into<String>) -> Self {
198        self.items.push(ImageListItem::new(label, image_source));
199        self
200    }
201
202    /// Add an image item with callback.
203    pub fn item_with_callback<F>(
204        mut self, 
205        label: impl Into<String>, 
206        image_source: impl Into<String>,
207        callback: F
208    ) -> Self 
209    where
210        F: Fn() + Send + Sync + 'static,
211    {
212        self.items.push(
213            ImageListItem::new(label, image_source)
214                .on_click(callback)
215        );
216        self
217    }
218
219    /// Set item spacing.
220    pub fn item_spacing(mut self, spacing: f32) -> Self {
221        self.item_spacing = spacing;
222        self
223    }
224
225    /// Enable text protection overlay.
226    pub fn text_protected(mut self, protected: bool) -> Self {
227        self.text_protected = protected;
228        self
229    }
230
231    /// Set corner radius.
232    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
233        self.corner_radius = corner_radius.into();
234        self
235    }
236
237    /// Set unique ID salt to prevent ID clashes.
238    pub fn id_salt(mut self, salt: impl Into<String>) -> Self {
239        self.id_salt = Some(salt.into());
240        self
241    }
242
243    /// Add items from a collection of file paths.
244    pub fn items_from_paths<I, P>(mut self, paths: I) -> Self
245    where
246        I: IntoIterator<Item = P>,
247        P: AsRef<std::path::Path>,
248    {
249        for (i, path) in paths.into_iter().enumerate() {
250            let path_str = path.as_ref().to_string_lossy().to_string();
251            let filename = if let Some(file_name) = path.as_ref().file_name() {
252                file_name.to_string_lossy().to_string()
253            } else {
254                format!("Image {}", i + 1)
255            };
256            self.items.push(ImageListItem::new(filename, path_str));
257        }
258        self
259    }
260
261    /// Add items from a collection of URLs.
262    pub fn items_from_urls<I, S>(mut self, urls: I) -> Self
263    where
264        I: IntoIterator<Item = S>,
265        S: Into<String>,
266    {
267        for (i, url) in urls.into_iter().enumerate() {
268            let url_str = url.into();
269            let label = format!("Online {}", i + 1);
270            self.items.push(ImageListItem::new(label, url_str));
271        }
272        self
273    }
274
275    /// Add items from a collection of byte arrays (for embedded images).
276    pub fn items_from_bytes<I>(mut self, bytes_collection: I) -> Self
277    where
278        I: IntoIterator<Item = Vec<u8>>,
279    {
280        for (i, bytes) in bytes_collection.into_iter().enumerate() {
281            let label = format!("Embedded {}", i + 1);
282            // Convert bytes to hex string for the bytes: protocol
283            let hex_string = format!("bytes:{}", hex::encode(bytes));
284            self.items.push(ImageListItem::new(label, hex_string));
285        }
286        self
287    }
288
289    fn get_image_list_style(&self) -> Color32 {
290        get_global_color("surface")
291    }
292}
293
294impl<'a> Default for MaterialImageList<'a> {
295    fn default() -> Self {
296        Self::standard()
297    }
298}
299
300impl Widget for MaterialImageList<'_> {
301    fn ui(self, ui: &mut Ui) -> Response {
302        let background_color = self.get_image_list_style();
303
304        let MaterialImageList {
305            variant,
306            items,
307            columns,
308            item_spacing,
309            text_protected,
310            corner_radius,
311            id_salt,
312            tmppath: _,
313        } = self;
314
315        if items.is_empty() {
316            return ui.allocate_response(Vec2::ZERO, Sense::hover());
317        }
318
319        // Calculate grid dimensions
320        let available_width = ui.available_width();
321        let item_width = (available_width - (columns - 1) as f32 * item_spacing) / columns as f32;
322        let item_height = match variant {
323            ImageListVariant::Standard => item_width, // Square items
324            ImageListVariant::Masonry => item_width * 1.2, // Slightly taller
325            ImageListVariant::Woven => item_width * 0.8, // Slightly shorter
326        };
327        
328        let rows = (items.len() + columns - 1) / columns;
329        let total_height = rows as f32 * (item_height + item_spacing) - item_spacing;
330        let total_width = available_width;
331
332        let response = ui.allocate_response(
333            Vec2::new(total_width, total_height), 
334            Sense::hover()
335        );
336        let rect = response.rect;
337
338        if ui.is_rect_visible(rect) {
339            // Draw background
340            ui.painter().rect_filled(rect, corner_radius, background_color);
341
342            // Draw items in grid
343            for (index, item) in items.iter().enumerate() {
344                let row = index / columns;
345                let col = index % columns;
346                
347                let item_x = rect.min.x + col as f32 * (item_width + item_spacing);
348                let item_y = rect.min.y + row as f32 * (item_height + item_spacing);
349                
350                let item_rect = Rect::from_min_size(
351                    egui::pos2(item_x, item_y),
352                    Vec2::new(item_width, item_height)
353                );
354
355                // Handle item interaction with unique ID
356                let item_id = if let Some(ref salt) = id_salt {
357                    egui::Id::new((salt, "image_item", index))
358                } else {
359                    egui::Id::new(("image_item", index, &item.label))
360                };
361                
362                let item_response = ui.interact(item_rect, item_id, Sense::click());
363                if item_response.hovered() {
364                    let hover_color = get_global_color("primary").linear_multiply(0.08);
365                    ui.painter().rect_filled(item_rect, corner_radius, hover_color);
366                }
367
368                if item_response.clicked() {
369                    if let Some(callback) = &item.on_click {
370                        callback();
371                    }
372                }
373
374                // Draw placeholder image (rectangle with border)
375                let image_rect = item_rect.shrink(2.0);
376                let image_bg = get_global_color("surfaceVariant");
377                let image_border = Stroke::new(1.0, get_global_color("outline"));
378                
379                ui.painter().rect_filled(image_rect, corner_radius, image_bg);
380                ui.painter().rect_stroke(image_rect, corner_radius, image_border, egui::epaint::StrokeKind::Outside);
381
382                // Draw image icon placeholder (camera icon representation)
383                // let icon_center = image_rect.center();
384                // let icon_color = get_global_color("onSurfaceVariant");
385                // ui.painter().circle_filled(icon_center, 16.0, icon_color);
386                // ui.painter().circle_filled(icon_center + Vec2::new(0.0, -4.0), 6.0, Color32::WHITE);
387                let mut failed = false;
388                if let Some(ref image_source) = item.image_source {
389                    let img_data = if image_source.starts_with("http://") || image_source.starts_with("https://") {
390                        #[cfg(feature = "ondemand")]
391                        {
392                            // Only print processing message if we're actually going to process
393                            let mut hasher = std::collections::hash_map::DefaultHasher::new();
394                            image_source.hash(&mut hasher);
395                            let url_hash = format!("{:x}", hasher.finish());
396                            let filename = format!("img_{}", url_hash);
397                            let filepath = std::path::Path::new(&self.tmppath).join(&filename);
398
399                            // Quick check if file already exists to avoid unnecessary processing messages
400                            let possible_files = [
401                                filepath.with_extension("png"),
402                                filepath.with_extension("jpg"),
403                                filepath.with_extension("gif"),
404                                filepath.with_extension("webp"),
405                                filepath.clone()
406                            ];
407                            let file_exists = possible_files.iter().any(|f| f.exists());
408
409                            if !file_exists {
410                                println!("Processing new HTTP URL: {}", image_source);
411                            } else {
412                                println!("Using cached image for: {}", image_source);
413                            }
414
415                            // Ensure cache directory exists
416                            if let Err(e) = std::fs::create_dir_all(&self.tmppath) {
417                                println!("Failed to create cache directory {}: {}", self.tmppath, e);
418                            } else {
419                                println!("Cache directory: {}", self.tmppath);
420                                println!("Target file path: {}", filepath.display());
421                            }
422
423                            // Check if file already exists with any extension
424                            let possible_files = [
425                                filepath.with_extension("png"),
426                                filepath.with_extension("jpg"),
427                                filepath.with_extension("gif"),
428                                filepath.with_extension("webp"),
429                                filepath.clone() // Original path without extension
430                            ];
431
432                            let existing_file = possible_files.iter().find(|f| f.exists());
433
434                            if existing_file.is_none() {
435                                println!("File does not exist, attempting download: {}", filepath.display());
436                                // Try to download the image with timeout and user agent
437                                let agent = ureq::AgentBuilder::new()
438                                    .timeout_read(std::time::Duration::from_secs(10))
439                                    .timeout_write(std::time::Duration::from_secs(10))
440                                    .user_agent("egui-material3/1.0")
441                                    .build();
442
443                                match agent.get(image_source).call() {
444                                    Ok(response) => {
445                                        let status = response.status();
446                                        println!("Download response status: {}", status);
447                                        if status == 200 {
448                                            let mut bytes = Vec::new();
449                                            match response.into_reader().read_to_end(&mut bytes) {
450                                                Ok(_) => {
451                                                    println!("Downloaded {} bytes", bytes.len());
452                                                    if !bytes.is_empty() {
453                                                        // Detect image format from content and add appropriate extension
454                                                        let extension = if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
455                                                            "png"
456                                                        } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
457                                                            "jpg"
458                                                        } else if bytes.starts_with(&[0x47, 0x49, 0x46]) {
459                                                            "gif"
460                                                        } else if bytes.starts_with(&[0x52, 0x49, 0x46, 0x46]) && bytes.len() > 12 && &bytes[8..12] == b"WEBP" {
461                                                            "webp"
462                                                        } else {
463                                                            println!("Unknown image format, defaulting to png");
464                                                            "png"
465                                                        };
466
467                                                        let filepath_with_ext = filepath.with_extension(extension);
468                                                        println!("Detected format: {}, saving as: {}", extension, filepath_with_ext.display());
469
470                                                        match std::fs::write(&filepath_with_ext, &bytes) {
471                                                            Ok(_) => {
472                                                                println!("Successfully saved to: {}", filepath_with_ext.display());
473                                                                // Only request repaint after successful download
474                                                                ui.ctx().request_repaint();
475                                                            }
476                                                            Err(e) => {
477                                                                println!("Failed to save file: {}", e);
478                                                            }
479                                                        }
480                                                    } else {
481                                                        println!("Downloaded 0 bytes - empty response");
482                                                    }
483                                                }
484                                                Err(e) => {
485                                                    println!("Failed to read response body: {}", e);
486                                                }
487                                            }
488                                        } else {
489                                            println!("HTTP error status: {}", status);
490                                        }
491                                    }
492                                    Err(e) => {
493                                        println!("Download failed: {}", e);
494                                    }
495                                }
496                            } else {
497                                println!("File already exists with extension: {:?}", existing_file.unwrap().display());
498                            }
499
500                            if let Some(existing_filepath) = existing_file {
501                                println!("Loading image from: {}", existing_filepath.display());
502                                match image::open(existing_filepath) {
503                                    Ok(image) => {
504                                        println!("Successfully opened image: {}x{}", image.width(), image.height());
505                                        let original_size = image.dimensions();
506
507                                        // Resize large images to max 512x512 to avoid memory issues
508                                        let resized_image = if original_size.0 > 512 || original_size.1 > 512 {
509                                            image.resize(512, 512, image::imageops::FilterType::Lanczos3)
510                                        } else {
511                                            image
512                                        };
513
514                                        let size = resized_image.dimensions();
515                                        let image_buffer = resized_image.to_rgba8();
516                                        let pixels = image_buffer.into_raw();
517                                        println!("Created ColorImage {}x{} with {} pixels", size.0, size.1, pixels.len());
518                                        // No need to request repaint for existing cached images
519                                        Some(egui::ColorImage::from_rgba_unmultiplied([size.0 as usize, size.1 as usize], &pixels))
520                                    }
521                                    Err(e) => {
522                                        println!("Failed to open image file: {}", e);
523                                        None
524                                    }
525                                }
526                            } else {
527                                println!("Image file does not exist after download attempt: {}", filepath.display());
528                                None
529                            }
530                        }
531                        #[cfg(not(feature = "ondemand"))]
532                        {
533                            println!("ondemand feature NOT enabled - cannot download HTTP URL");
534                            None
535                        }
536                    } else if image_source.starts_with("data:") {
537                        // Handle data URLs (base64 encoded images)
538                        if let Some(comma_pos) = image_source.find(',') {
539                            let data_part = &image_source[comma_pos + 1..];
540                            if let Ok(bytes) = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data_part) {
541                                if let Ok(image) = image::load_from_memory(&bytes) {
542                                    let size = image.dimensions();
543                                    let image_buffer = image.to_rgba8();
544                                    let pixels = image_buffer.into_raw();
545                                    Some(egui::ColorImage::from_rgba_unmultiplied([size.0 as usize, size.1 as usize], &pixels))
546                                } else {
547                                    None
548                                }
549                            } else {
550                                None
551                            }
552                        } else {
553                            None
554                        }
555                    } else if image_source.starts_with("bytes:") {
556                        // Handle raw bytes string (hex encoded or similar)
557                        let bytes_str = &image_source[6..]; // Remove "bytes:" prefix
558                        if let Ok(bytes) = hex::decode(bytes_str) {
559                            if let Ok(image) = image::load_from_memory(&bytes) {
560                                let size = image.dimensions();
561                                let image_buffer = image.to_rgba8();
562                                let pixels = image_buffer.into_raw();
563                                Some(egui::ColorImage::from_rgba_unmultiplied([size.0 as usize, size.1 as usize], &pixels))
564                            } else {
565                                None
566                            }
567                        } else {
568                            None
569                        }
570                    } else if std::path::Path::new(image_source).exists() {
571                        if let Ok(image) = image::open(image_source) {
572                            let size = image.dimensions();
573                            let image_buffer = image.to_rgba8();
574                            let pixels = image_buffer.into_raw();
575                            Some(egui::ColorImage::from_rgba_unmultiplied([size.0 as usize, size.1 as usize], &pixels))
576                        } else {
577                            None
578                        }
579                    } else {
580                        None
581                    };
582                    
583                    if let Some(color_image) = img_data {
584                        let texture_name = format!("image_texture_{}_{}", item_id.value(), item.label);
585                        let texture_id = ui.ctx().load_texture(
586                            texture_name,
587                            color_image,
588                            Default::default()
589                        );
590                        ui.painter().image(
591                            texture_id.id(),
592                            image_rect,
593                            egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
594                            Color32::WHITE,
595                        );
596                    } else {
597                        failed = true;
598                    }
599                } else {
600                    // Default placeholder when no image source
601                    failed = true;
602                    // let img_data = egui::include_image!("../resources/320x240.png");
603                    // let img = egui::Image::new(img_data)
604                    //     .fit_to_exact_size(image_rect.size())
605                    //     .corner_radius(corner_radius);
606
607                    // ui.put(image_rect, img);
608                }
609
610                if failed {
611                    // Debug: Print when showing X marks
612                    println!("SHOWING X MARK for item: {}", item.label);
613                    // Draw a simple "X" to indicate failed image load
614                    let line_color = get_global_color("error");
615                    ui.painter().line_segment(
616                        [image_rect.min, image_rect.max],
617                        Stroke::new(2.0, line_color),
618                    );
619                    ui.painter().line_segment(
620                        [egui::pos2(image_rect.min.x, image_rect.max.y), egui::pos2(image_rect.max.x, image_rect.min.y)],
621                        Stroke::new(2.0, line_color),
622                    );
623                }
624
625                // Draw text overlay or below image
626                let text_color = if text_protected {
627                    Color32::WHITE
628                } else {
629                    get_global_color("onSurface")
630                };
631
632                if text_protected {
633                    // Draw dark overlay for text protection
634                    let overlay_rect = Rect::from_min_size(
635                        egui::pos2(image_rect.min.x, image_rect.max.y - 40.0),
636                        Vec2::new(image_rect.width(), 40.0)
637                    );
638                    let overlay_color = Color32::from_rgba_unmultiplied(0, 0, 0, 128);
639                    ui.painter().rect_filled(overlay_rect, CornerRadius::ZERO, overlay_color);
640                    
641                    // Draw text on overlay
642                    let text_pos = egui::pos2(image_rect.min.x + 8.0, image_rect.max.y - 30.0);
643                    ui.painter().text(
644                        text_pos,
645                        egui::Align2::LEFT_TOP,
646                        &item.label,
647                        egui::FontId::proportional(12.0),
648                        text_color
649                    );
650                    
651                    if let Some(supporting_text) = &item.supporting_text {
652                        let support_text_pos = egui::pos2(image_rect.min.x + 8.0, image_rect.max.y - 16.0);
653                        ui.painter().text(
654                            support_text_pos,
655                            egui::Align2::LEFT_TOP,
656                            supporting_text,
657                            egui::FontId::proportional(10.0),
658                            get_global_color("onSurfaceVariant")
659                        );
660                    }
661                } else {
662                    // Draw text below image
663                    let text_y = item_rect.max.y - 30.0;
664                    let text_pos = egui::pos2(item_rect.min.x + 4.0, text_y);
665                    
666                    ui.painter().text(
667                        text_pos,
668                        egui::Align2::LEFT_TOP,
669                        &item.label,
670                        egui::FontId::proportional(12.0),
671                        text_color
672                    );
673                    // draw image_source if avalilable
674                    if let Some(image_source) = &item.image_source {
675                        let source_pos = egui::pos2(item_rect.min.x + 4.0, text_y + 14.0);
676                        ui.painter().text(
677                            source_pos,
678                            egui::Align2::LEFT_TOP,
679                            image_source,
680                            egui::FontId::proportional(10.0),
681                            get_global_color("onSurfaceVariant")
682                        );
683                    }
684                    
685                    if let Some(supporting_text) = &item.supporting_text {
686                        let support_text_pos = egui::pos2(item_rect.min.x + 4.0, text_y + 14.0);
687                        ui.painter().text(
688                            support_text_pos,
689                            egui::Align2::LEFT_TOP,
690                            supporting_text,
691                            egui::FontId::proportional(10.0),
692                            get_global_color("onSurfaceVariant")
693                        );
694                    }
695                }
696            }
697        }
698
699        response
700    }
701}
702
703/// Convenience function to create a standard image list.
704pub fn image_list() -> MaterialImageList<'static> {
705    MaterialImageList::standard()
706}
707
708/// Convenience function to create a masonry image list.
709pub fn masonry_image_list() -> MaterialImageList<'static> {
710    MaterialImageList::masonry()
711}
712
713/// Convenience function to create a woven image list.
714pub fn woven_image_list() -> MaterialImageList<'static> {
715    MaterialImageList::woven()
716}