notedeck

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

badge.rs (10369B)


      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     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::hover());
    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             // Background
    157             painter.rect_filled(rect, rounding, bg_color);
    158 
    159             // Text (offset left if keybind present)
    160             let text_offset_x = if self.keybind.is_some() {
    161                 -keybind_extra / 2.0
    162             } else {
    163                 0.0
    164             };
    165             let text_pos = rect.center() + Vec2::new(text_offset_x, 0.0) - galley.size() / 2.0;
    166             painter.galley(text_pos, galley, text_color);
    167 
    168             // Draw keybind box if present
    169             if let Some(key) = self.keybind {
    170                 let box_center = egui::pos2(
    171                     rect.right() - padding.x - keybind_box_size / 2.0,
    172                     rect.center().y,
    173                 );
    174                 let box_rect =
    175                     egui::Rect::from_center_size(box_center, Vec2::splat(keybind_box_size));
    176 
    177                 // Keybind box background (slightly darker/lighter than badge bg)
    178                 let visuals = ui.visuals();
    179                 let box_bg = visuals.widgets.noninteractive.bg_fill;
    180                 let box_stroke = text_color.gamma_multiply(0.5);
    181 
    182                 painter.rect_filled(box_rect, 3.0, box_bg);
    183                 painter.rect_stroke(
    184                     box_rect,
    185                     3.0,
    186                     egui::Stroke::new(1.0, box_stroke),
    187                     egui::StrokeKind::Inside,
    188                 );
    189 
    190                 // Keybind text
    191                 painter.text(
    192                     box_center + Vec2::new(0.0, 1.0),
    193                     egui::Align2::CENTER_CENTER,
    194                     key,
    195                     egui::FontId::monospace(keybind_box_size * 0.65),
    196                     visuals.text_color(),
    197                 );
    198             }
    199         }
    200 
    201         response
    202     }
    203 }
    204 
    205 /// A pill-shaped action button with integrated keybind hint
    206 pub struct ActionButton<'a> {
    207     text: &'a str,
    208     bg_color: Color32,
    209     text_color: Color32,
    210     keybind: Option<&'a str>,
    211 }
    212 
    213 impl<'a> ActionButton<'a> {
    214     /// Create a new action button with the given text and colors
    215     pub fn new(text: &'a str, bg_color: Color32, text_color: Color32) -> Self {
    216         Self {
    217             text,
    218             bg_color,
    219             text_color,
    220             keybind: None,
    221         }
    222     }
    223 
    224     /// Add a keybind hint inside the button (e.g., "1" for key 1)
    225     pub fn keybind(mut self, key: &'a str) -> Self {
    226         self.keybind = Some(key);
    227         self
    228     }
    229 
    230     /// Show the button and return the response
    231     pub fn show(self, ui: &mut Ui) -> Response {
    232         // Calculate text size for proper allocation
    233         let font_id = egui::FontId::proportional(13.0);
    234         let galley =
    235             ui.painter()
    236                 .layout_no_wrap(self.text.to_string(), font_id.clone(), self.text_color);
    237 
    238         // Calculate keybind box size if present
    239         let keybind_box_size = 16.0;
    240         let keybind_spacing = 6.0;
    241         let keybind_extra = if self.keybind.is_some() {
    242             keybind_box_size + keybind_spacing
    243         } else {
    244             0.0
    245         };
    246 
    247         // Padding: horizontal 10px, vertical 4px
    248         let padding = Vec2::new(10.0, 4.0);
    249         let desired_size =
    250             Vec2::new(galley.size().x + keybind_extra, galley.size().y) + padding * 2.0;
    251 
    252         let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
    253 
    254         if ui.is_rect_visible(rect) {
    255             let painter = ui.painter();
    256 
    257             // Adjust color based on hover/click state
    258             let bg_color = if response.is_pointer_button_down_on() {
    259                 self.bg_color.gamma_multiply(0.8)
    260             } else if response.hovered() {
    261                 self.bg_color.gamma_multiply(1.15)
    262             } else {
    263                 self.bg_color
    264             };
    265 
    266             // Full pill rounding (half of height)
    267             let rounding = rect.height() / 2.0;
    268 
    269             // Background
    270             painter.rect_filled(rect, rounding, bg_color);
    271 
    272             // Text (offset right if keybind present, since keybind goes on left)
    273             let text_offset_x = if self.keybind.is_some() {
    274                 keybind_extra / 2.0
    275             } else {
    276                 0.0
    277             };
    278             let text_pos = rect.center() + Vec2::new(text_offset_x, 0.0) - galley.size() / 2.0;
    279             painter.galley(text_pos, galley, self.text_color);
    280 
    281             // Draw keybind hint on left side (white border, no fill)
    282             if let Some(key) = self.keybind {
    283                 let box_center = egui::pos2(
    284                     rect.left() + padding.x + keybind_box_size / 2.0,
    285                     rect.center().y,
    286                 );
    287                 let box_rect =
    288                     egui::Rect::from_center_size(box_center, Vec2::splat(keybind_box_size));
    289 
    290                 // White border only
    291                 painter.rect_stroke(
    292                     box_rect,
    293                     3.0,
    294                     egui::Stroke::new(1.0, Color32::WHITE),
    295                     egui::StrokeKind::Inside,
    296                 );
    297 
    298                 // Keybind text with vertical nudge for optical centering
    299                 painter.text(
    300                     box_center + Vec2::new(0.0, 1.0),
    301                     egui::Align2::CENTER_CENTER,
    302                     key,
    303                     egui::FontId::monospace(keybind_box_size * 0.7),
    304                     self.text_color,
    305                 );
    306             }
    307         }
    308 
    309         response
    310     }
    311 }