1use crate::{get_global_color, material_symbol::material_symbol_text};
2use egui::{
3 ecolor::Color32,
4 emath::NumExt,
5 epaint::{CornerRadius, Shadow, Stroke},
6 Align, Image, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo,
7 WidgetText, WidgetType,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
59pub enum MaterialButtonVariant {
60 Filled,
62 Outlined,
64 Text,
66 Elevated,
68 FilledTonal,
70}
71
72#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
81pub struct MaterialButton<'a> {
82 image: Option<Image<'a>>,
84 text: Option<WidgetText>,
86 shortcut_text: WidgetText,
88 wrap_mode: Option<TextWrapMode>,
90
91 variant: MaterialButtonVariant,
93 fill: Option<Color32>,
95 stroke: Option<Stroke>,
97 sense: Sense,
99 small: bool,
101 frame: Option<bool>,
103 min_size: Vec2,
105 corner_radius: Option<CornerRadius>,
107 selected: bool,
109 image_tint_follows_text_color: bool,
116 elevation: Option<Shadow>,
118 disabled: bool,
120 leading_icon: Option<String>,
122 trailing_icon: Option<String>,
124 leading_svg: Option<String>,
126 trailing_svg: Option<String>,
128 text_color: Option<Color32>,
130}
131
132impl<'a> MaterialButton<'a> {
133 pub fn filled(text: impl Into<WidgetText>) -> Self {
144 Self::new_with_variant(MaterialButtonVariant::Filled, text)
145 }
146
147 pub fn outlined(text: impl Into<WidgetText>) -> Self {
158 Self::new_with_variant(MaterialButtonVariant::Outlined, text)
159 }
160
161 pub fn text(text: impl Into<WidgetText>) -> Self {
172 Self::new_with_variant(MaterialButtonVariant::Text, text)
173 }
174
175 pub fn elevated(text: impl Into<WidgetText>) -> Self {
186 Self::new_with_variant(MaterialButtonVariant::Elevated, text).elevation(Shadow {
187 offset: [0, 2],
188 blur: 6,
189 spread: 0,
190 color: Color32::from_rgba_unmultiplied(0, 0, 0, 30),
191 })
192 }
193
194 pub fn filled_tonal(text: impl Into<WidgetText>) -> Self {
205 Self::new_with_variant(MaterialButtonVariant::FilledTonal, text)
206 }
207
208 fn new_with_variant(variant: MaterialButtonVariant, text: impl Into<WidgetText>) -> Self {
210 Self::opt_image_and_text_with_variant(variant, None, Some(text.into()))
211 }
212
213 pub fn new(text: impl Into<WidgetText>) -> Self {
214 Self::filled(text)
215 }
216
217 #[allow(clippy::needless_pass_by_value)]
219 pub fn image(image: impl Into<Image<'a>>) -> Self {
220 Self::opt_image_and_text(Some(image.into()), None)
221 }
222
223 #[allow(clippy::needless_pass_by_value)]
225 pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
226 Self::opt_image_and_text(Some(image.into()), Some(text.into()))
227 }
228
229 pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
237 Self::opt_image_and_text_with_variant(MaterialButtonVariant::Filled, image, text)
238 }
239
240 pub fn opt_image_and_text_with_variant(
249 variant: MaterialButtonVariant,
250 image: Option<Image<'a>>,
251 text: Option<WidgetText>,
252 ) -> Self {
253 Self {
254 variant,
255 text,
256 image,
257 shortcut_text: Default::default(),
258 wrap_mode: None,
259 fill: None,
260 stroke: None,
261 sense: Sense::click(),
262 small: false,
263 frame: None,
264 min_size: Vec2::ZERO,
265 corner_radius: None,
266 selected: false,
267 image_tint_follows_text_color: false,
268 elevation: None,
269 disabled: false,
270 leading_icon: None,
271 trailing_icon: None,
272 leading_svg: None,
273 trailing_svg: None,
274 text_color: None,
275 }
276 }
277
278 #[inline]
284 pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
285 self.wrap_mode = Some(wrap_mode);
286 self
287 }
288
289 #[inline]
291 pub fn wrap(mut self) -> Self {
292 self.wrap_mode = Some(TextWrapMode::Wrap);
293
294 self
295 }
296
297 #[inline]
299 pub fn truncate(mut self) -> Self {
300 self.wrap_mode = Some(TextWrapMode::Truncate);
301 self
302 }
303
304 #[inline]
307 pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
308 self.fill = Some(fill.into());
309 self.frame = Some(true);
310 self
311 }
312
313 #[inline]
316 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
317 self.stroke = Some(stroke.into());
318 self.frame = Some(true);
319 self
320 }
321
322 #[inline]
324 pub fn small(mut self) -> Self {
325 if let Some(text) = self.text {
326 self.text = Some(text.text_style(TextStyle::Body));
327 }
328 self.small = true;
329 self
330 }
331
332 #[inline]
334 pub fn frame(mut self, frame: bool) -> Self {
335 self.frame = Some(frame);
336 self
337 }
338
339 #[inline]
342 pub fn sense(mut self, sense: Sense) -> Self {
343 self.sense = sense;
344 self
345 }
346
347 #[inline]
349 pub fn min_size(mut self, min_size: Vec2) -> Self {
350 self.min_size = min_size;
351 self
352 }
353
354 #[inline]
356 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
357 self.corner_radius = Some(corner_radius.into());
358 self
359 }
360
361 #[inline]
362 #[deprecated = "Renamed to `corner_radius`"]
363 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
364 self.corner_radius(corner_radius)
365 }
366
367 #[inline]
374 pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
375 self.image_tint_follows_text_color = image_tint_follows_text_color;
376 self
377 }
378
379 #[inline]
385 pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
386 self.shortcut_text = shortcut_text.into();
387 self
388 }
389
390 #[inline]
392 pub fn selected(mut self, selected: bool) -> Self {
393 self.selected = selected;
394 self
395 }
396
397 #[inline]
399 pub fn enabled(mut self, enabled: bool) -> Self {
400 self.disabled = !enabled;
401 self
402 }
403
404 #[inline]
406 pub fn elevation(mut self, elevation: Shadow) -> Self {
407 self.elevation = Some(elevation);
408 self
409 }
410
411 #[inline]
415 pub fn leading_icon(mut self, icon: impl Into<String>) -> Self {
416 self.leading_icon = Some(icon.into());
417 self
418 }
419
420 #[inline]
424 pub fn trailing_icon(mut self, icon: impl Into<String>) -> Self {
425 self.trailing_icon = Some(icon.into());
426 self
427 }
428
429 #[inline]
433 pub fn leading_svg(mut self, svg_data: impl Into<String>) -> Self {
434 self.leading_svg = Some(svg_data.into());
435 self
436 }
437
438 #[inline]
442 pub fn trailing_svg(mut self, svg_data: impl Into<String>) -> Self {
443 self.trailing_svg = Some(svg_data.into());
444 self
445 }
446
447 #[inline]
452 pub fn text_color(mut self, color: Color32) -> Self {
453 self.text_color = Some(color);
454 self
455 }
456}
457
458impl Widget for MaterialButton<'_> {
459 fn ui(self, ui: &mut Ui) -> Response {
460 let MaterialButton {
461 variant,
462 text,
463 image,
464 shortcut_text,
465 wrap_mode,
466 fill,
467 stroke,
468 sense,
469 small,
470 frame,
471 min_size,
472 corner_radius,
473 selected,
474 image_tint_follows_text_color,
475 elevation,
476 disabled,
477 leading_icon,
478 trailing_icon,
479 leading_svg,
480 trailing_svg,
481 text_color: custom_text_color,
482 } = self;
483
484 let md_primary = get_global_color("primary");
486 let _md_surface_tint = get_global_color("surfaceTint");
487 let _md_on_primary = get_global_color("onPrimary");
488 let _md_primary_container = get_global_color("primaryContainer");
489 let _md_on_primary_container = get_global_color("onPrimaryContainer");
490 let _md_secondary = get_global_color("secondary");
491 let _md_on_secondary = get_global_color("onSecondary");
492 let _md_secondary_container = get_global_color("secondaryContainer");
493 let _md_on_secondary_container = get_global_color("onSecondaryContainer");
494 let _md_tertiary = get_global_color("tertiary");
495 let _md_on_tertiary = get_global_color("onTertiary");
496 let _md_tertiary_container = get_global_color("tertiaryContainer");
497 let _md_on_tertiary_container = get_global_color("onTertiaryContainer");
498 let _md_error = get_global_color("error");
499 let _md_on_error = get_global_color("onError");
500 let _md_error_container = get_global_color("errorContainer");
501 let _md_on_error_container = get_global_color("onErrorContainer");
502 let md_background = get_global_color("background");
503 let md_on_background = get_global_color("onBackground");
504 let md_surface = get_global_color("surface");
505 let md_on_surface = get_global_color("onSurface");
506 let md_surface_variant = get_global_color("surfaceVariant");
507 let _md_on_surface_variant = get_global_color("onSurfaceVariant");
508 let md_outline = get_global_color("outline");
509 let _md_outline_variant = get_global_color("outlineVariant");
510 let _md_shadow = get_global_color("shadow");
511 let _md_scrim = get_global_color("scrim");
512 let _md_inverse_surface = get_global_color("inverseSurface");
513 let _md_inverse_on_surface = get_global_color("inverseOnSurface");
514 let _md_inverse_primary = get_global_color("inversePrimary");
515 let _md_primary_fixed = get_global_color("primaryFixed");
516 let _md_on_primary_fixed = get_global_color("onPrimaryFixed");
517 let _md_primary_fixed_dim = get_global_color("primaryFixedDim");
518 let _md_on_primary_fixed_variant = get_global_color("onPrimaryFixedVariant");
519 let _md_secondary_fixed = get_global_color("secondaryFixed");
520 let _md_on_secondary_fixed = get_global_color("onSecondaryFixed");
521 let _md_secondary_fixed_dim = get_global_color("secondaryFixedDim");
522 let _md_on_secondary_fixed_variant = get_global_color("onSecondaryFixedVariant");
523 let _md_tertiary_fixed = get_global_color("tertiaryFixed");
524 let _md_on_tertiary_fixed = get_global_color("onTertiaryFixed");
525 let _md_tertiary_fixed_dim = get_global_color("tertiaryFixedDim");
526 let _md_on_tertiary_fixed_variant = get_global_color("onTertiaryFixedVariant");
527 let _md_surface_dim = get_global_color("surfaceDim");
528 let _md_surface_bright = get_global_color("surfaceBright");
529 let _md_surface_container_lowest = get_global_color("surfaceContainerLowest");
530 let _md_surface_container_low = get_global_color("surfaceContainerLow");
531 let _md_surface_container = get_global_color("surfaceContainer");
532 let _md_surface_container_high = get_global_color("surfaceContainerHigh");
533 let _md_surface_container_highest = get_global_color("surfaceContainerHighest");
534
535 let (default_fill, default_stroke, default_corner_radius, _has_elevation) = match variant {
537 MaterialButtonVariant::Filled => (
538 Some(md_primary),
539 Some(Stroke::NONE),
540 CornerRadius::from(20),
541 false,
542 ),
543 MaterialButtonVariant::Outlined => (
544 Some(Color32::TRANSPARENT),
545 Some(Stroke::new(1.0, md_outline)),
546 CornerRadius::from(20),
547 false,
548 ),
549 MaterialButtonVariant::Text => (
550 Some(Color32::TRANSPARENT),
551 Some(Stroke::NONE),
552 CornerRadius::from(20),
553 false,
554 ),
555 MaterialButtonVariant::Elevated => (
556 Some(md_surface),
557 Some(Stroke::NONE),
558 CornerRadius::from(20),
559 true,
560 ),
561 MaterialButtonVariant::FilledTonal => (
562 Some(md_surface_variant),
563 Some(Stroke::NONE),
564 CornerRadius::from(20),
565 false,
566 ),
567 };
568
569 let frame = frame.unwrap_or_else(|| match variant {
570 MaterialButtonVariant::Text => false,
571 _ => true,
572 });
573
574 let leading_svg_texture = leading_svg.and_then(|svg_data| {
576 crate::image_utils::create_texture_from_svg(ui.ctx(), &svg_data, &format!("btn_lead_{}", svg_data.len())).ok()
577 });
578 let trailing_svg_texture = trailing_svg.and_then(|svg_data| {
579 crate::image_utils::create_texture_from_svg(ui.ctx(), &svg_data, &format!("btn_trail_{}", svg_data.len())).ok()
580 });
581
582 let leading_icon_galley = if leading_svg_texture.is_none() {
584 leading_icon.map(|name| {
585 let icon_str: WidgetText = material_symbol_text(&name).into();
586 icon_str.into_galley(ui, Some(TextWrapMode::Extend), f32::INFINITY, TextStyle::Body)
587 })
588 } else {
589 None
590 };
591 let trailing_icon_galley = if trailing_svg_texture.is_none() {
592 trailing_icon.map(|name| {
593 let icon_str: WidgetText = material_symbol_text(&name).into();
594 icon_str.into_galley(ui, Some(TextWrapMode::Extend), f32::INFINITY, TextStyle::Body)
595 })
596 } else {
597 None
598 };
599
600 let has_leading = leading_icon_galley.is_some() || leading_svg_texture.is_some() || image.is_some();
607 let has_trailing = trailing_icon_galley.is_some() || trailing_svg_texture.is_some();
608 let padding_multiplier = if small { 0.25 } else { 1.0 };
609 let padding_left = if has_leading { 16.0 } else { 24.0 } * padding_multiplier;
610 let padding_right = if has_trailing { 16.0 } else { 24.0 } * padding_multiplier;
611 let button_padding_left;
612 let button_padding_right;
613 let button_padding_y;
614 if frame || variant == MaterialButtonVariant::Text {
615 button_padding_left = padding_left;
616 button_padding_right = padding_right;
617 button_padding_y = if small { 4.0 } else { 10.0 };
618 } else {
619 button_padding_left = 0.0;
620 button_padding_right = 0.0;
621 button_padding_y = 0.0;
622 }
623
624 let min_button_height = if small { 32.0 } else { 40.0 };
626 let icon_spacing = if small { 4.0 } else { 8.0 }; let svg_icon_size = 18.0; let resolved_text_color = if disabled {
631 md_background.gamma_multiply(0.38)
632 } else if let Some(custom) = custom_text_color {
633 custom
634 } else {
635 match variant {
636 MaterialButtonVariant::Filled => md_background,
637 MaterialButtonVariant::Outlined => md_on_background,
638 MaterialButtonVariant::Text => md_on_background,
639 MaterialButtonVariant::Elevated => md_on_background,
640 MaterialButtonVariant::FilledTonal => get_global_color("onSecondaryContainer"),
641 }
642 };
643
644 let space_available_for_image = if let Some(_text) = &text {
645 let font_height = ui.text_style_height(&TextStyle::Body);
646 Vec2::splat(font_height)
647 } else {
648 let total_h_padding = button_padding_left + button_padding_right;
649 ui.available_size() - Vec2::new(total_h_padding, 2.0 * button_padding_y)
650 };
651
652 let image_size = if let Some(image) = &image {
653 image
654 .load_and_calc_size(ui, space_available_for_image)
655 .unwrap_or(space_available_for_image)
656 } else {
657 Vec2::ZERO
658 };
659
660 let gap_before_shortcut_text = ui.spacing().item_spacing.x;
661
662 let mut text_wrap_width = ui.available_width() - button_padding_left - button_padding_right;
663 if image.is_some() {
664 text_wrap_width -= image_size.x + icon_spacing;
665 }
666 if leading_icon_galley.is_some() {
667 text_wrap_width -= leading_icon_galley.as_ref().unwrap().size().x + icon_spacing;
668 }
669 if leading_svg_texture.is_some() {
670 text_wrap_width -= svg_icon_size + icon_spacing;
671 }
672 if trailing_icon_galley.is_some() {
673 text_wrap_width -= trailing_icon_galley.as_ref().unwrap().size().x + icon_spacing;
674 }
675 if trailing_svg_texture.is_some() {
676 text_wrap_width -= svg_icon_size + icon_spacing;
677 }
678
679 let shortcut_galley = (!shortcut_text.is_empty()).then(|| {
681 shortcut_text.into_galley(
682 ui,
683 Some(TextWrapMode::Extend),
684 f32::INFINITY,
685 TextStyle::Body,
686 )
687 });
688
689 if let Some(shortcut_galley) = &shortcut_galley {
690 text_wrap_width -= gap_before_shortcut_text + shortcut_galley.size().x;
691 }
692
693 let galley =
694 text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Body));
695
696 let mut desired_size = Vec2::ZERO;
697
698 if let Some(lg) = &leading_icon_galley {
700 desired_size.x += lg.size().x;
701 desired_size.y = desired_size.y.max(lg.size().y);
702 }
703 if leading_svg_texture.is_some() {
704 desired_size.x += svg_icon_size;
705 desired_size.y = desired_size.y.max(svg_icon_size);
706 }
707
708 if image.is_some() {
710 if leading_icon_galley.is_some() || leading_svg_texture.is_some() {
711 desired_size.x += icon_spacing;
712 }
713 desired_size.x += image_size.x;
714 desired_size.y = desired_size.y.max(image_size.y);
715 }
716
717 if (leading_icon_galley.is_some() || leading_svg_texture.is_some() || image.is_some()) && galley.is_some() {
719 desired_size.x += icon_spacing;
720 }
721
722 if let Some(galley) = &galley {
723 desired_size.x += galley.size().x;
724 desired_size.y = desired_size.y.max(galley.size().y);
725 }
726
727 if let Some(tg) = &trailing_icon_galley {
729 if galley.is_some() || image.is_some() || leading_icon_galley.is_some() || leading_svg_texture.is_some() {
730 desired_size.x += icon_spacing;
731 }
732 desired_size.x += tg.size().x;
733 desired_size.y = desired_size.y.max(tg.size().y);
734 }
735 if trailing_svg_texture.is_some() {
736 if galley.is_some() || image.is_some() || leading_icon_galley.is_some() || leading_svg_texture.is_some() {
737 desired_size.x += icon_spacing;
738 }
739 desired_size.x += svg_icon_size;
740 desired_size.y = desired_size.y.max(svg_icon_size);
741 }
742
743 if let Some(shortcut_galley) = &shortcut_galley {
744 desired_size.x += gap_before_shortcut_text + shortcut_galley.size().x;
745 desired_size.y = desired_size.y.max(shortcut_galley.size().y);
746 }
747
748 desired_size.x += button_padding_left + button_padding_right;
749 desired_size.y += 2.0 * button_padding_y;
750 if !small {
751 desired_size.y = desired_size.y.at_least(min_button_height);
752 }
753 desired_size = desired_size.at_least(min_size);
754
755 let (rect, response) = ui.allocate_at_least(desired_size, sense);
756 response.widget_info(|| {
757 if let Some(galley) = &galley {
758 WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
759 } else {
760 WidgetInfo::new(WidgetType::Button)
761 }
762 });
763
764 if ui.is_rect_visible(rect) {
765 let visuals = ui.style().interact(&response);
766
767 let (frame_expansion, _frame_cr, frame_fill, frame_stroke) = if selected {
768 let selection = ui.visuals().selection;
769 (
770 Vec2::ZERO,
771 CornerRadius::ZERO,
772 selection.bg_fill,
773 selection.stroke,
774 )
775 } else if frame {
776 let expansion = Vec2::splat(visuals.expansion);
777 (
778 expansion,
779 visuals.corner_radius,
780 visuals.weak_bg_fill,
781 visuals.bg_stroke,
782 )
783 } else {
784 Default::default()
785 };
786 let frame_cr = corner_radius.unwrap_or(default_corner_radius);
787 let mut frame_fill = fill.unwrap_or(default_fill.unwrap_or(frame_fill));
788 let mut frame_stroke = stroke.unwrap_or(default_stroke.unwrap_or(frame_stroke));
789
790 if disabled {
792 let surface_color = get_global_color("surface");
793 frame_fill = surface_color;
794 frame_stroke.color = md_on_surface.gamma_multiply(0.12);
795 frame_stroke.width = if matches!(variant, MaterialButtonVariant::Outlined) {
796 1.0
797 } else {
798 0.0
799 };
800 }
801
802 if !disabled {
804 let state_layer_color = resolved_text_color;
805 if response.is_pointer_button_down_on() {
806 frame_fill = blend_overlay(frame_fill, state_layer_color, 0.12);
808 } else if response.hovered() {
809 frame_fill = blend_overlay(frame_fill, state_layer_color, 0.08);
811 }
812 }
813
814 if let Some(shadow) = &elevation {
816 let shadow = if !disabled && response.hovered() {
818 Shadow {
819 offset: [shadow.offset[0], shadow.offset[1] + 2],
820 blur: shadow.blur + 4,
821 spread: shadow.spread,
822 color: shadow.color,
823 }
824 } else {
825 *shadow
826 };
827 let shadow_offset = Vec2::new(shadow.offset[0] as f32, shadow.offset[1] as f32);
828 let shadow_rect = rect.expand2(frame_expansion).translate(shadow_offset);
829 ui.painter()
830 .rect_filled(shadow_rect, frame_cr, shadow.color);
831 }
832
833 ui.painter().rect(
834 rect.expand2(frame_expansion),
835 frame_cr,
836 frame_fill,
837 frame_stroke,
838 egui::epaint::StrokeKind::Outside,
839 );
840
841 let mut cursor_x = rect.min.x + button_padding_left;
842 let content_rect_y_min = rect.min.y + button_padding_y;
843 let content_rect_y_max = rect.max.y - button_padding_y;
844 let content_height = content_rect_y_max - content_rect_y_min;
845
846 if let Some(leading_galley) = &leading_icon_galley {
848 let icon_y =
849 content_rect_y_min + (content_height - leading_galley.size().y) / 2.0;
850 let icon_pos = egui::pos2(cursor_x, icon_y);
851 ui.painter()
852 .galley(icon_pos, leading_galley.clone(), resolved_text_color);
853 cursor_x += leading_galley.size().x + icon_spacing;
854 }
855
856 if let Some(texture) = &leading_svg_texture {
858 let icon_y = content_rect_y_min + (content_height - svg_icon_size) / 2.0;
859 let icon_rect = Rect::from_min_size(
860 egui::pos2(cursor_x, icon_y),
861 Vec2::splat(svg_icon_size),
862 );
863 ui.painter().image(
864 texture.id(),
865 icon_rect,
866 Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
867 Color32::WHITE, );
869 cursor_x += svg_icon_size;
870 if image.is_some() || galley.is_some() || trailing_icon_galley.is_some() || trailing_svg_texture.is_some() || shortcut_galley.is_some() {
872 cursor_x += icon_spacing;
873 }
874 }
875
876 if let Some(image) = &image {
878 let mut image_pos = ui
879 .layout()
880 .align_size_within_rect(
881 image_size,
882 Rect::from_min_max(
883 egui::pos2(cursor_x, content_rect_y_min),
884 egui::pos2(rect.max.x - button_padding_right, content_rect_y_max),
885 ),
886 )
887 .min;
888 if galley.is_some() || shortcut_galley.is_some() || trailing_icon_galley.is_some() {
889 image_pos.x = cursor_x;
890 }
891 let image_rect = Rect::from_min_size(image_pos, image_size);
892 cursor_x += image_size.x + icon_spacing;
893 let mut image_widget = image.clone();
894 if image_tint_follows_text_color {
895 image_widget = image_widget.tint(visuals.text_color());
896 }
897 image_widget.paint_at(ui, image_rect);
898 }
899
900 let has_text = galley.is_some();
902 if let Some(galley) = galley {
903 let text_y = content_rect_y_min + (content_height - galley.size().y) / 2.0 + if small { 1.0 } else { 0.0 };
904 let mut text_pos = egui::pos2(cursor_x, text_y);
905 if leading_icon_galley.is_none()
907 && leading_svg_texture.is_none()
908 && image.is_none()
909 && trailing_icon_galley.is_none()
910 && trailing_svg_texture.is_none()
911 && shortcut_galley.is_none()
912 {
913 text_pos = ui
914 .layout()
915 .align_size_within_rect(
916 galley.size(),
917 Rect::from_min_max(
918 egui::pos2(
919 rect.min.x + button_padding_left,
920 content_rect_y_min,
921 ),
922 egui::pos2(
923 rect.max.x - button_padding_right,
924 content_rect_y_max,
925 ),
926 ),
927 )
928 .min;
929 }
930
931 cursor_x = text_pos.x + galley.size().x;
932 ui.painter().galley(text_pos, galley, resolved_text_color);
933 }
934
935 if let Some(trailing_galley) = &trailing_icon_galley {
937 cursor_x += icon_spacing;
938 let icon_y =
939 content_rect_y_min + (content_height - trailing_galley.size().y) / 2.0;
940 let icon_pos = egui::pos2(cursor_x, icon_y);
941 ui.painter()
942 .galley(icon_pos, trailing_galley.clone(), resolved_text_color);
943 }
944
945 if let Some(texture) = &trailing_svg_texture {
947 if has_text || image.is_some() || leading_icon_galley.is_some() || leading_svg_texture.is_some() {
949 cursor_x += icon_spacing;
950 }
951 let icon_y = content_rect_y_min + (content_height - svg_icon_size) / 2.0;
952 let icon_rect = Rect::from_min_size(
953 egui::pos2(cursor_x, icon_y),
954 Vec2::splat(svg_icon_size),
955 );
956 ui.painter().image(
957 texture.id(),
958 icon_rect,
959 Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
960 Color32::WHITE, );
962 }
963
964 if let Some(shortcut_galley) = shortcut_galley {
966 let layout = if ui.layout().is_horizontal() {
967 ui.layout().with_main_align(Align::Max)
968 } else {
969 ui.layout().with_cross_align(Align::Max)
970 };
971 let shortcut_text_pos = layout
972 .align_size_within_rect(
973 shortcut_galley.size(),
974 Rect::from_min_max(
975 egui::pos2(rect.min.x + button_padding_left, content_rect_y_min),
976 egui::pos2(rect.max.x - button_padding_right, content_rect_y_max),
977 ),
978 )
979 .min;
980 ui.painter().galley(
981 shortcut_text_pos,
982 shortcut_galley,
983 ui.visuals().weak_text_color(),
984 );
985 }
986 }
987
988 if let Some(cursor) = ui.visuals().interact_cursor {
989 if response.hovered() {
990 ui.ctx().set_cursor_icon(cursor);
991 }
992 }
993
994 response
995 }
996}
997
998fn blend_overlay(base: Color32, overlay: Color32, opacity: f32) -> Color32 {
1000 let alpha = (opacity * 255.0) as u8;
1001 let overlay_with_alpha = Color32::from_rgba_unmultiplied(overlay.r(), overlay.g(), overlay.b(), alpha);
1002 let inv_alpha = 255 - alpha;
1004 Color32::from_rgba_unmultiplied(
1005 ((base.r() as u16 * inv_alpha as u16 + overlay_with_alpha.r() as u16 * alpha as u16) / 255) as u8,
1006 ((base.g() as u16 * inv_alpha as u16 + overlay_with_alpha.g() as u16 * alpha as u16) / 255) as u8,
1007 ((base.b() as u16 * inv_alpha as u16 + overlay_with_alpha.b() as u16 * alpha as u16) / 255) as u8,
1008 base.a(),
1009 )
1010}