egui_material3/
chips.rs

1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget, TextureHandle};
2use crate::{get_global_color, image_utils};
3
4/// Material Design chip variants following Material Design 3 specifications
5#[derive(Clone, Copy, PartialEq)]
6pub enum ChipVariant {
7    /// Assist chips help users take actions or get information about their current context
8    Assist,
9    /// Filter chips let users refine content by selecting or deselecting options
10    Filter,
11    /// Input chips represent discrete pieces of information entered by a user
12    Input,
13    /// Suggestion chips help users discover relevant, actionable content
14    Suggestion,
15}
16
17/// Types of icons that can be displayed in chips
18#[derive(Clone)]
19pub enum IconType {
20    /// Material Design icon using icon name or unicode
21    MaterialIcon(String),
22    /// Custom SVG icon data
23    SvgData(String),
24    /// PNG image data as bytes
25    PngBytes(Vec<u8>),
26    /// Pre-loaded egui texture handle
27    Texture(TextureHandle),
28}
29
30/// Material Design chip component following Material Design 3 specifications
31/// 
32/// Chips are compact elements that represent an input, attribute, or action.
33/// They allow users to enter information, make selections, filter content, or trigger actions.
34/// 
35/// ## Usage Examples
36/// ```rust
37/// # egui::__run_test_ui(|ui| {
38/// // Assist chip - helps users with contextual actions
39/// if ui.add(MaterialChip::assist("Settings")).clicked() {
40///     // Open settings
41/// }
42/// 
43/// // Filter chip - for filtering content
44/// let mut filter_active = false;
45/// ui.add(MaterialChip::filter("Photos")
46///     .selected(&mut filter_active));
47/// 
48/// // Input chip - represents entered data
49/// ui.add(MaterialChip::input("john@example.com")
50///     .removable(true));
51/// 
52/// // Suggestion chip - suggests actions or content
53/// ui.add(MaterialChip::suggestion("Try this feature"));
54/// # });
55/// ```
56/// 
57/// ## Material Design Spec
58/// - Height: 32dp (standard) or 24dp (small)
59/// - Corner radius: 8dp (filter/input) or 16dp (assist/suggestion)
60/// - Text: Label Large (14sp/500 weight)
61/// - Touch target: Minimum 48x48dp
62pub struct MaterialChip<'a> {
63    /// Text content displayed on the chip
64    text: String,
65    /// Which type of chip this is (affects styling and behavior)
66    variant: ChipVariant,
67    /// Optional mutable reference to selection state (for filter chips)
68    selected: Option<&'a mut bool>,
69    /// Whether the chip is interactive
70    enabled: bool,
71    /// Whether the chip is soft-disabled (different visual treatment)
72    soft_disabled: bool,
73    /// Whether the chip has elevation shadow
74    elevated: bool,
75    /// Whether the chip can be removed (shows X icon)
76    removable: bool,
77    /// Optional leading icon to display
78    leading_icon: Option<IconType>,
79    /// Whether to use avatar-style rounded appearance
80    avatar: bool,
81    /// Optional action callback when chip is clicked
82    action: Option<Box<dyn Fn() + 'a>>,
83}
84
85impl<'a> MaterialChip<'a> {
86    /// Create a new chip with specified text and variant
87    /// 
88    /// ## Parameters
89    /// - `text`: Text to display on the chip
90    /// - `variant`: Type of chip (Assist, Filter, Input, Suggestion)
91    pub fn new(text: impl Into<String>, variant: ChipVariant) -> Self {
92        Self {
93            text: text.into(),
94            variant,
95            selected: None,
96            enabled: true,
97            soft_disabled: false,
98            elevated: false,
99            removable: false,
100            leading_icon: None,
101            avatar: false, // regular chips are more rectangular by default
102            action: None,
103        }
104    }
105
106    /// Create an assist chip for contextual actions
107    /// 
108    /// Assist chips help users take actions or get information about their current context.
109    /// They should appear dynamically and contextually in the UI.
110    /// 
111    /// ## Material Design Usage
112    /// - Display contextually relevant actions
113    /// - Usually triggered by user actions or context changes  
114    /// - Should not be persistent in the interface
115    pub fn assist(text: impl Into<String>) -> Self {
116        Self::new(text, ChipVariant::Assist)
117    }
118
119    /// Create a filter chip for content filtering
120    /// 
121    /// Filter chips are used for filtering content and are typically displayed in a set.
122    /// They can be selected/deselected to refine displayed content.
123    /// 
124    /// ## Parameters
125    /// - `text`: Label for the filter option
126    /// - `selected`: Mutable reference to selection state
127    /// 
128    /// ## Material Design Usage
129    /// - Group related filter options together
130    /// - Allow multiple selections for broad filtering
131    /// - Provide clear visual feedback for selected state
132    pub fn filter(text: impl Into<String>, selected: &'a mut bool) -> Self {
133        let mut chip = Self::new(text, ChipVariant::Filter);
134        chip.selected = Some(selected);
135        chip
136    }
137
138    /// Create an input chip representing user-entered data
139    /// 
140    /// Input chips represent discrete pieces of information entered by a user,
141    /// such as tags, contacts, or other structured data.
142    /// 
143    /// ## Material Design Usage
144    /// - Represent complex entities in a compact form
145    /// - Often removable to allow editing of input data
146    /// - Used in forms and data entry interfaces
147    pub fn input(text: impl Into<String>) -> Self {
148        Self::new(text, ChipVariant::Input)
149    }
150
151    /// Create a suggestion chip that provides actionable content suggestions
152    /// 
153    /// Suggestion chips are used to help users discover relevant actions or content.
154    /// They can be used in conjunction with dynamic features like autocomplete or
155    /// content recommendations.
156    pub fn suggestion(text: impl Into<String>) -> Self {
157        Self::new(text, ChipVariant::Suggestion)
158    }
159
160    /// Set whether the chip should have elevation (shadow) effect
161    /// 
162    /// Elevated chips have a surface-container-high background and a shadow
163    /// to indicate elevation. This is typically used for assist and suggestion chips.
164    pub fn elevated(mut self, elevated: bool) -> Self {
165        self.elevated = elevated;
166        self
167    }
168
169    /// Enable or disable the chip
170    /// 
171    /// Disabled chips have a different visual treatment and do not respond to
172    /// user interactions. Soft-disabled chips are still visible but appear
173    /// with reduced opacity.
174    pub fn enabled(mut self, enabled: bool) -> Self {
175        self.enabled = enabled;
176        if enabled {
177            self.soft_disabled = false; // if enabled, can't be soft disabled
178        }
179        self
180    }
181
182    /// Set the chip as soft-disabled
183    /// 
184    /// Soft-disabled chips have a different visual treatment (e.g., lighter opacity)
185    /// compared to hard-disabled chips. They are still interactive but indicate
186    /// that the action is unavailable.
187    pub fn soft_disabled(mut self, soft_disabled: bool) -> Self {
188        self.soft_disabled = soft_disabled;
189        if soft_disabled {
190            self.enabled = false; // soft disabled means not enabled
191        }
192        self
193    }
194
195    /// Set whether the chip can be removed
196    /// 
197    /// Removable chips show an X icon that allows users to remove the chip
198    /// from the UI. This is useful for input and filter chips.
199    pub fn removable(mut self, removable: bool) -> Self {
200        self.removable = removable;
201        self
202    }
203
204    /// Set a leading icon for the chip using a Material icon name
205    /// 
206    /// The icon will be displayed on the left side of the chip's text.
207    /// This is commonly used for assist and filter chips.
208    pub fn leading_icon(mut self, icon: impl Into<String>) -> Self {
209        self.leading_icon = Some(IconType::MaterialIcon(icon.into()));
210        self
211    }
212
213    /// Set a leading icon for the chip using SVG data
214    /// 
215    /// The SVG data will be converted to a texture and displayed on the left
216    /// side of the chip's text. This allows for custom icons with scalable
217    /// vector graphics.
218    pub fn leading_icon_svg(mut self, svg_data: impl Into<String>) -> Self {
219        self.leading_icon = Some(IconType::SvgData(svg_data.into()));
220        self
221    }
222
223    /// Set a leading icon for the chip using PNG image data
224    /// 
225    /// The PNG image data will be converted to a texture and displayed on the left
226    /// side of the chip's text. This is useful for using raster images as icons.
227    pub fn leading_icon_png(mut self, png_bytes: Vec<u8>) -> Self {
228        self.leading_icon = Some(IconType::PngBytes(png_bytes));
229        self
230    }
231
232    /// Set a pre-loaded texture as the leading icon for the chip
233    /// 
234    /// This allows using any texture as an icon, without the need to convert
235    /// from image data. The texture should be created and managed externally.
236    pub fn leading_icon_texture(mut self, texture: TextureHandle) -> Self {
237        self.leading_icon = Some(IconType::Texture(texture));
238        self
239    }
240
241    /// Set whether to use avatar-style rounded appearance for the chip
242    /// 
243    /// Avatar-style chips have a more pronounced roundness, making them suitable
244    /// for representing users or profile-related content. Regular chips are more
245    /// rectangular.
246    pub fn avatar(mut self, avatar: bool) -> Self {
247        self.avatar = avatar;
248        self
249    }
250
251    /// Set a callback function to be called when the chip is clicked
252    /// 
253    /// This allows defining custom actions for each chip, such as navigating to
254    /// a different view, opening a dialog, or triggering any other behavior.
255    pub fn on_click<F>(mut self, f: F) -> Self 
256    where
257        F: Fn() + 'a,
258    {
259        self.action = Some(Box::new(f));
260        self
261    }
262}
263
264impl<'a> Widget for MaterialChip<'a> {
265    fn ui(self, ui: &mut Ui) -> Response {
266        let text_width = ui.fonts(|fonts| {
267            fonts.layout_no_wrap(
268                self.text.clone(),
269                egui::FontId::default(),
270                egui::Color32::WHITE
271            ).rect.width()
272        });
273        
274        let icon_width = if self.leading_icon.is_some() || (self.variant == ChipVariant::Filter && self.selected.as_ref().map_or(false, |s| **s)) {
275            24.0
276        } else {
277            0.0
278        };
279        
280        let remove_width = if self.removable { 24.0 } else { 0.0 };
281        let padding = 16.0;
282        
283        let desired_size = Vec2::new(
284            (text_width + icon_width + remove_width + padding).min(ui.available_width()),
285            32.0,
286        );
287
288        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
289        
290        // Track interaction states for state layers
291        let is_pressed = response.is_pointer_button_down_on();
292        let is_hovered = response.hovered();
293
294        // Material Design colors
295        let primary_color = get_global_color("primary");
296        let _surface = get_global_color("surface");
297        let surface_variant = get_global_color("surfaceVariant");
298        let _surface_container_low = get_global_color("surfaceContainerLow");
299        let surface_container_high = get_global_color("surfaceContainerHigh");
300        let on_surface = get_global_color("onSurface");
301        let on_surface_variant = get_global_color("onSurfaceVariant");
302        let outline = get_global_color("outline");
303        let _error = get_global_color("error");
304
305        let (bg_color, border_color, text_color, state_layer_color) = match self.variant {
306            ChipVariant::Assist => {
307                if !self.enabled {
308                    if self.soft_disabled {
309                        // Soft-disabled: lighter opacity, different from hard disabled
310                        (
311                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 50), // 20% opacity
312                            Color32::TRANSPARENT,
313                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 153), // 60% opacity
314                            Color32::TRANSPARENT, // No state layer for disabled
315                        )
316                    } else {
317                        // Hard disabled state: on-surface with 12% opacity for container, 38% for text
318                        (
319                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 31), // 12% opacity
320                            Color32::TRANSPARENT,
321                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 97), // 38% opacity
322                            Color32::TRANSPARENT, // No state layer for disabled
323                        )
324                    }
325                } else if self.elevated {
326                    // Elevated: surface-container-high background
327                    let state_layer = if is_pressed {
328                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
329                    } else if is_hovered {
330                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
331                    } else {
332                        Color32::TRANSPARENT
333                    };
334                    (surface_container_high, Color32::TRANSPARENT, on_surface_variant, state_layer)
335                } else {
336                    // Default: surface-variant background
337                    let state_layer = if is_pressed {
338                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
339                    } else if is_hovered {
340                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
341                    } else {
342                        Color32::TRANSPARENT
343                    };
344                    (surface_variant, outline, on_surface_variant, state_layer)
345                }
346            }
347            ChipVariant::Filter => {
348                let is_selected = self.selected.as_ref().map_or(false, |s| **s);
349                if !self.enabled {
350                    if self.soft_disabled {
351                        // Soft-disabled: lighter opacity, different from hard disabled
352                        (
353                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 50), // 20% opacity
354                            Color32::TRANSPARENT,
355                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 153), // 60% opacity
356                            Color32::TRANSPARENT, // No state layer for disabled
357                        )
358                    } else {
359                        // Hard disabled state: on-surface with 12% opacity for container, 38% for text
360                        (
361                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 31), // 12% opacity
362                            Color32::TRANSPARENT,
363                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 97), // 38% opacity
364                            Color32::TRANSPARENT, // No state layer for disabled
365                        )
366                    }
367                } else if is_selected {
368                    // Selected: secondary container background with primary border
369                    let state_layer = if is_pressed {
370                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
371                    } else if is_hovered {
372                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
373                    } else {
374                        Color32::TRANSPARENT
375                    };
376                    (
377                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 24), // Light primary background
378                        primary_color,
379                        on_surface,
380                        state_layer,
381                    )
382                } else if self.elevated {
383                    // Elevated: surface-container-high background
384                    let state_layer = if is_pressed {
385                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
386                    } else if is_hovered {
387                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
388                    } else {
389                        Color32::TRANSPARENT
390                    };
391                    (surface_container_high, Color32::TRANSPARENT, on_surface_variant, state_layer)
392                } else {
393                    // Default: surface-variant background
394                    let state_layer = if is_pressed {
395                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
396                    } else if is_hovered {
397                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
398                    } else {
399                        Color32::TRANSPARENT
400                    };
401                    (surface_variant, outline, on_surface_variant, state_layer)
402                }
403            }
404            ChipVariant::Input => {
405                if !self.enabled {
406                    if self.soft_disabled {
407                        // Soft-disabled Input chips: different opacity from other chips
408                        (
409                            Color32::from_rgba_premultiplied(surface_variant.r(), surface_variant.g(), surface_variant.b(), 80), // 30% opacity
410                            Color32::TRANSPARENT,
411                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 180), // 70% opacity
412                            Color32::TRANSPARENT, // No state layer for disabled
413                        )
414                    } else {
415                        // Hard disabled Input chips: slightly different from other chips
416                        (
417                            Color32::from_rgba_premultiplied(surface_variant.r(), surface_variant.g(), surface_variant.b(), 40), // 15% opacity
418                            Color32::TRANSPARENT,
419                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 120), // 47% opacity
420                            Color32::TRANSPARENT, // No state layer for disabled
421                        )
422                    }
423                } else if self.elevated {
424                    // Elevated: surface-container-high background
425                    let state_layer = if is_pressed {
426                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
427                    } else if is_hovered {
428                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
429                    } else {
430                        Color32::TRANSPARENT
431                    };
432                    (surface_container_high, Color32::TRANSPARENT, on_surface_variant, state_layer)
433                } else {
434                    // Default: surface-variant background
435                    let state_layer = if is_pressed {
436                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
437                    } else if is_hovered {
438                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
439                    } else {
440                        Color32::TRANSPARENT
441                    };
442                    (surface_variant, outline, on_surface_variant, state_layer)
443                }
444            }
445            ChipVariant::Suggestion => {
446                if !self.enabled {
447                    if self.soft_disabled {
448                        // Soft-disabled: lighter opacity, different from hard disabled
449                        (
450                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 50), // 20% opacity
451                            Color32::TRANSPARENT,
452                            Color32::from_rgba_premultiplied(on_surface_variant.r(), on_surface_variant.g(), on_surface_variant.b(), 153), // 60% opacity
453                            Color32::TRANSPARENT, // No state layer for disabled
454                        )
455                    } else {
456                        // Hard disabled state: on-surface with 12% opacity for container, 38% for text
457                        (
458                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 31), // 12% opacity
459                            Color32::TRANSPARENT,
460                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 97), // 38% opacity
461                            Color32::TRANSPARENT, // No state layer for disabled
462                        )
463                    }
464                } else if self.elevated {
465                    // Elevated: surface-container-high background
466                    let state_layer = if is_pressed {
467                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
468                    } else if is_hovered {
469                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
470                    } else {
471                        Color32::TRANSPARENT
472                    };
473                    (surface_container_high, Color32::TRANSPARENT, on_surface_variant, state_layer)
474                } else {
475                    // Default: surface-variant background
476                    let state_layer = if is_pressed {
477                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 31) // 12% opacity for pressed
478                    } else if is_hovered {
479                        Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20) // 8% opacity for hover
480                    } else {
481                        Color32::TRANSPARENT
482                    };
483                    (surface_variant, outline, on_surface_variant, state_layer)
484                }
485            }
486        };
487
488        // Calculate corner radius - avatar chips are more rounded, regular chips are more rectangular
489        let corner_radius = if self.avatar { 16.0 } else { 8.0 };
490
491        // Draw chip background
492        ui.painter().rect_filled(
493            rect,
494            corner_radius,
495            bg_color,
496        );
497
498        // Draw state layer (hover/pressed overlay)
499        if state_layer_color != Color32::TRANSPARENT {
500            ui.painter().rect_filled(
501                rect,
502                corner_radius,
503                state_layer_color,
504            );
505        }
506
507        // Draw chip border (if not transparent)
508        if border_color != Color32::TRANSPARENT {
509            ui.painter().rect_stroke(
510                rect,
511                corner_radius,
512                Stroke::new(1.0, border_color),
513                egui::epaint::StrokeKind::Outside,
514            );
515        }
516
517        // Draw elevation shadow for elevated chips (before background) - but not for disabled chips
518        if self.elevated && self.enabled {
519            let shadow_offset = Vec2::new(0.0, 2.0);
520            let shadow_rect = rect.translate(shadow_offset);
521            ui.painter().rect_filled(
522                shadow_rect,
523                corner_radius,
524                Color32::from_rgba_unmultiplied(0, 0, 0, 30),
525            );
526        }
527
528        // Layout content
529        let mut content_x = rect.min.x + 8.0;
530        
531        // Draw leading icon or checkmark
532        if let Some(icon) = &self.leading_icon {
533            let icon_rect = Rect::from_min_size(
534                Pos2::new(content_x, rect.center().y - 10.0),
535                Vec2::splat(20.0),
536            );
537            
538            match icon {
539                IconType::MaterialIcon(icon_str) => {
540                    // Draw material icon using proper icon system
541                    let icon = crate::icon::MaterialIcon::new(icon_str)
542                        .size(16.0)
543                        .color(text_color);
544                    ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
545                        ui.add(icon);
546                    });
547                }
548                IconType::SvgData(svg_data) => {
549                    // Convert SVG to texture and draw
550                    if let Ok(texture) = image_utils::create_texture_from_svg(ui.ctx(), svg_data, &format!("chip_svg_{}", svg_data.len())) {
551                        ui.painter().image(
552                            texture.id(),
553                            icon_rect,
554                            Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
555                            Color32::WHITE,
556                        );
557                    }
558                }
559                IconType::PngBytes(png_bytes) => {
560                    // Convert PNG bytes to texture and draw
561                    if let Ok(texture) = image_utils::create_texture_from_png_bytes(ui.ctx(), png_bytes, &format!("chip_png_{}", png_bytes.len())) {
562                        ui.painter().image(
563                            texture.id(),
564                            icon_rect,
565                            Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
566                            Color32::WHITE,
567                        );
568                    }
569                }
570                IconType::Texture(texture) => {
571                    // Draw pre-loaded texture
572                    ui.painter().image(
573                        texture.id(),
574                        icon_rect,
575                        Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
576                        Color32::WHITE,
577                    );
578                }
579            }
580            content_x += 24.0;
581        } else if self.variant == ChipVariant::Filter && self.selected.as_ref().map_or(false, |s| **s) {
582            // Draw checkmark for selected filter chips
583            let icon_rect = Rect::from_min_size(
584                Pos2::new(content_x, rect.center().y - 10.0),
585                Vec2::splat(20.0),
586            );
587            
588            let center = icon_rect.center();
589            let checkmark_size = 12.0;
590            
591            let start = Pos2::new(
592                center.x - checkmark_size * 0.3,
593                center.y,
594            );
595            let middle = Pos2::new(
596                center.x - checkmark_size * 0.1,
597                center.y + checkmark_size * 0.2,
598            );
599            let end = Pos2::new(
600                center.x + checkmark_size * 0.3,
601                center.y - checkmark_size * 0.2,
602            );
603
604            ui.painter().line_segment([start, middle], Stroke::new(2.0, text_color));
605            ui.painter().line_segment([middle, end], Stroke::new(2.0, text_color));
606            content_x += 24.0;
607        }
608
609        // Draw text
610        let text_pos = Pos2::new(content_x, rect.center().y);
611        ui.painter().text(
612            text_pos,
613            egui::Align2::LEFT_CENTER,
614            &self.text,
615            egui::FontId::default(),
616            text_color,
617        );
618
619        // Draw remove button for removable chips
620        if self.removable {
621            let remove_rect = Rect::from_min_size(
622                Pos2::new(rect.max.x - 24.0, rect.center().y - 10.0),
623                Vec2::splat(20.0),
624            );
625            
626            // Draw X
627            let center = remove_rect.center();
628            let cross_size = 8.0;
629            ui.painter().line_segment([
630                Pos2::new(center.x - cross_size / 2.0, center.y - cross_size / 2.0),
631                Pos2::new(center.x + cross_size / 2.0, center.y + cross_size / 2.0),
632            ], Stroke::new(1.5, text_color));
633            ui.painter().line_segment([
634                Pos2::new(center.x + cross_size / 2.0, center.y - cross_size / 2.0),
635                Pos2::new(center.x - cross_size / 2.0, center.y + cross_size / 2.0),
636            ], Stroke::new(1.5, text_color));
637        }
638
639        // Handle interactions
640        if response.clicked() && self.enabled {
641            match self.variant {
642                ChipVariant::Filter => {
643                    if let Some(selected) = self.selected {
644                        *selected = !*selected;
645                        response.mark_changed();
646                    }
647                }
648                _ => {
649                    if let Some(action) = self.action {
650                        action();
651                    }
652                }
653            }
654        }
655
656
657        response
658    }
659}
660
661pub fn assist_chip(text: impl Into<String>) -> MaterialChip<'static> {
662    MaterialChip::assist(text)
663}
664
665pub fn filter_chip(text: impl Into<String>, selected: &mut bool) -> MaterialChip<'_> {
666    MaterialChip::filter(text, selected)
667}
668
669pub fn input_chip(text: impl Into<String>) -> MaterialChip<'static> {
670    MaterialChip::input(text)
671}
672
673pub fn suggestion_chip(text: impl Into<String>) -> MaterialChip<'static> {
674    MaterialChip::suggestion(text)
675}