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}