notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

badge.rs (10703B)


      1 use egui::{Color32, Response, Ui, Vec2};
      2 
      3 /// Badge variants that determine the color scheme
      4 #[derive(Clone, Copy, Default)]
      5 #[allow(dead_code)]
      6 pub enum BadgeVariant {
      7     /// Default muted style
      8     #[default]
      9     Default,
     10     /// Informational blue
     11     Info,
     12     /// Success green
     13     Success,
     14     /// Warning amber/yellow
     15     Warning,
     16     /// Error/danger red
     17     Destructive,
     18 }
     19 
     20 impl BadgeVariant {
     21     /// Get background and text colors for this variant
     22     pub fn colors(&self, ui: &Ui) -> (Color32, Color32) {
     23         let is_dark = ui.visuals().dark_mode;
     24 
     25         match self {
     26             BadgeVariant::Default => {
     27                 let bg = if is_dark {
     28                     Color32::from_rgba_unmultiplied(255, 255, 255, 20)
     29                 } else {
     30                     Color32::from_rgba_unmultiplied(0, 0, 0, 15)
     31                 };
     32                 let fg = ui.visuals().text_color();
     33                 (bg, fg)
     34             }
     35             BadgeVariant::Info => {
     36                 // Blue tones
     37                 let bg = if is_dark {
     38                     Color32::from_rgba_unmultiplied(59, 130, 246, 30)
     39                 } else {
     40                     Color32::from_rgba_unmultiplied(59, 130, 246, 25)
     41                 };
     42                 let fg = if is_dark {
     43                     Color32::from_rgb(147, 197, 253) // blue-300
     44                 } else {
     45                     Color32::from_rgb(29, 78, 216) // blue-700
     46                 };
     47                 (bg, fg)
     48             }
     49             BadgeVariant::Success => {
     50                 // Green tones
     51                 let bg = if is_dark {
     52                     Color32::from_rgba_unmultiplied(34, 197, 94, 30)
     53                 } else {
     54                     Color32::from_rgba_unmultiplied(34, 197, 94, 25)
     55                 };
     56                 let fg = if is_dark {
     57                     Color32::from_rgb(134, 239, 172) // green-300
     58                 } else {
     59                     Color32::from_rgb(21, 128, 61) // green-700
     60                 };
     61                 (bg, fg)
     62             }
     63             BadgeVariant::Warning => {
     64                 // Amber/yellow tones
     65                 let bg = if is_dark {
     66                     Color32::from_rgba_unmultiplied(245, 158, 11, 30)
     67                 } else {
     68                     Color32::from_rgba_unmultiplied(245, 158, 11, 25)
     69                 };
     70                 let fg = if is_dark {
     71                     Color32::from_rgb(252, 211, 77) // amber-300
     72                 } else {
     73                     Color32::from_rgb(180, 83, 9) // amber-700
     74                 };
     75                 (bg, fg)
     76             }
     77             BadgeVariant::Destructive => {
     78                 // Red tones
     79                 let bg = if is_dark {
     80                     Color32::from_rgba_unmultiplied(239, 68, 68, 30)
     81                 } else {
     82                     Color32::from_rgba_unmultiplied(239, 68, 68, 25)
     83                 };
     84                 let fg = if is_dark {
     85                     Color32::from_rgb(252, 165, 165) // red-300
     86                 } else {
     87                     Color32::from_rgb(185, 28, 28) // red-700
     88                 };
     89                 (bg, fg)
     90             }
     91         }
     92     }
     93 }
     94 
     95 /// A pill-shaped status badge widget (shadcn-style)
     96 pub struct StatusBadge<'a> {
     97     text: &'a str,
     98     variant: BadgeVariant,
     99     keybind: Option<&'a str>,
    100 }
    101 
    102 impl<'a> StatusBadge<'a> {
    103     /// Create a new status badge with the given text
    104     pub fn new(text: &'a str) -> Self {
    105         Self {
    106             text,
    107             variant: BadgeVariant::Default,
    108             keybind: None,
    109         }
    110     }
    111 
    112     /// Set the badge variant
    113     pub fn variant(mut self, variant: BadgeVariant) -> Self {
    114         self.variant = variant;
    115         self
    116     }
    117 
    118     /// Add a keybind hint inside the badge (e.g., "P" for Ctrl+P)
    119     pub fn keybind(mut self, key: &'a str) -> Self {
    120         self.keybind = Some(key);
    121         self
    122     }
    123 
    124     /// Show the badge and return the response
    125     pub fn show(self, ui: &mut Ui) -> Response {
    126         let (bg_color, text_color) = self.variant.colors(ui);
    127 
    128         // Calculate text size for proper allocation
    129         let font_id = egui::FontId::proportional(11.0);
    130         let galley =
    131             ui.painter()
    132                 .layout_no_wrap(self.text.to_string(), font_id.clone(), text_color);
    133 
    134         // Calculate keybind box size if present
    135         let keybind_box_size = 14.0;
    136         let keybind_spacing = 5.0;
    137         let keybind_extra = if self.keybind.is_some() {
    138             keybind_box_size + keybind_spacing
    139         } else {
    140             0.0
    141         };
    142 
    143         // Padding: horizontal 8px, vertical 2px
    144         let padding = Vec2::new(8.0, 3.0);
    145         let desired_size =
    146             Vec2::new(galley.size().x + keybind_extra, galley.size().y) + padding * 2.0;
    147 
    148         let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
    149 
    150         if ui.is_rect_visible(rect) {
    151             let painter = ui.painter();
    152 
    153             // Full pill rounding (half of height)
    154             let rounding = rect.height() / 2.0;
    155 
    156             // Adjust background color based on hover/click state
    157             let bg_color = if response.is_pointer_button_down_on() {
    158                 bg_color.gamma_multiply(1.8)
    159             } else if response.hovered() {
    160                 bg_color.gamma_multiply(1.4)
    161             } else {
    162                 bg_color
    163             };
    164 
    165             // Background
    166             painter.rect_filled(rect, rounding, bg_color);
    167 
    168             // Text (offset left if keybind present)
    169             let text_offset_x = if self.keybind.is_some() {
    170                 -keybind_extra / 2.0
    171             } else {
    172                 0.0
    173             };
    174             let text_pos = rect.center() + Vec2::new(text_offset_x, 0.0) - galley.size() / 2.0;
    175             painter.galley(text_pos, galley, text_color);
    176 
    177             // Draw keybind box if present
    178             if let Some(key) = self.keybind {
    179                 let box_center = egui::pos2(
    180                     rect.right() - padding.x - keybind_box_size / 2.0,
    181                     rect.center().y,
    182                 );
    183                 let box_rect =
    184                     egui::Rect::from_center_size(box_center, Vec2::splat(keybind_box_size));
    185 
    186                 // Keybind box background (slightly darker/lighter than badge bg)
    187                 let visuals = ui.visuals();
    188                 let box_bg = visuals.widgets.noninteractive.bg_fill;
    189                 let box_stroke = text_color.gamma_multiply(0.5);
    190 
    191                 painter.rect_filled(box_rect, 3.0, box_bg);
    192                 painter.rect_stroke(
    193                     box_rect,
    194                     3.0,
    195                     egui::Stroke::new(1.0, box_stroke),
    196                     egui::StrokeKind::Inside,
    197                 );
    198 
    199                 // Keybind text
    200                 painter.text(
    201                     box_center + Vec2::new(0.0, 1.0),
    202                     egui::Align2::CENTER_CENTER,
    203                     key,
    204                     egui::FontId::monospace(keybind_box_size * 0.65),
    205                     visuals.text_color(),
    206                 );
    207             }
    208         }
    209 
    210         response
    211     }
    212 }
    213 
    214 /// A pill-shaped action button with integrated keybind hint
    215 pub struct ActionButton<'a> {
    216     text: &'a str,
    217     bg_color: Color32,
    218     text_color: Color32,
    219     keybind: Option<&'a str>,
    220 }
    221 
    222 impl<'a> ActionButton<'a> {
    223     /// Create a new action button with the given text and colors
    224     pub fn new(text: &'a str, bg_color: Color32, text_color: Color32) -> Self {
    225         Self {
    226             text,
    227             bg_color,
    228             text_color,
    229             keybind: None,
    230         }
    231     }
    232 
    233     /// Add a keybind hint inside the button (e.g., "1" for key 1)
    234     pub fn keybind(mut self, key: &'a str) -> Self {
    235         self.keybind = Some(key);
    236         self
    237     }
    238 
    239     /// Show the button and return the response
    240     pub fn show(self, ui: &mut Ui) -> Response {
    241         // Calculate text size for proper allocation
    242         let font_id = egui::FontId::proportional(13.0);
    243         let galley =
    244             ui.painter()
    245                 .layout_no_wrap(self.text.to_string(), font_id.clone(), self.text_color);
    246 
    247         // Calculate keybind box size if present
    248         let keybind_box_size = 16.0;
    249         let keybind_spacing = 6.0;
    250         let keybind_extra = if self.keybind.is_some() {
    251             keybind_box_size + keybind_spacing
    252         } else {
    253             0.0
    254         };
    255 
    256         // Padding: horizontal 10px, vertical 4px
    257         let padding = Vec2::new(10.0, 4.0);
    258         let desired_size =
    259             Vec2::new(galley.size().x + keybind_extra, galley.size().y) + padding * 2.0;
    260 
    261         let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
    262 
    263         if ui.is_rect_visible(rect) {
    264             let painter = ui.painter();
    265 
    266             // Adjust color based on hover/click state
    267             let bg_color = if response.is_pointer_button_down_on() {
    268                 self.bg_color.gamma_multiply(0.8)
    269             } else if response.hovered() {
    270                 self.bg_color.gamma_multiply(1.15)
    271             } else {
    272                 self.bg_color
    273             };
    274 
    275             // Full pill rounding (half of height)
    276             let rounding = rect.height() / 2.0;
    277 
    278             // Background
    279             painter.rect_filled(rect, rounding, bg_color);
    280 
    281             // Text (offset right if keybind present, since keybind goes on left)
    282             let text_offset_x = if self.keybind.is_some() {
    283                 keybind_extra / 2.0
    284             } else {
    285                 0.0
    286             };
    287             let text_pos = rect.center() + Vec2::new(text_offset_x, 0.0) - galley.size() / 2.0;
    288             painter.galley(text_pos, galley, self.text_color);
    289 
    290             // Draw keybind hint on left side (white border, no fill)
    291             if let Some(key) = self.keybind {
    292                 let box_center = egui::pos2(
    293                     rect.left() + padding.x + keybind_box_size / 2.0,
    294                     rect.center().y,
    295                 );
    296                 let box_rect =
    297                     egui::Rect::from_center_size(box_center, Vec2::splat(keybind_box_size));
    298 
    299                 // White border only
    300                 painter.rect_stroke(
    301                     box_rect,
    302                     3.0,
    303                     egui::Stroke::new(1.0, Color32::WHITE),
    304                     egui::StrokeKind::Inside,
    305                 );
    306 
    307                 // Keybind text with vertical nudge for optical centering
    308                 painter.text(
    309                     box_center + Vec2::new(0.0, 1.0),
    310                     egui::Align2::CENTER_CENTER,
    311                     key,
    312                     egui::FontId::monospace(keybind_box_size * 0.7),
    313                     self.text_color,
    314                 );
    315             }
    316         }
    317 
    318         response
    319     }
    320 }