notedeck

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

commit b7aa609f928a0dbb024336bb0797c978cd2c64a9
parent bd455002d06724d096f5bc1202c91c5800c2f073
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 27 Jan 2026 08:52:44 -0800

dave: add shadcn-style status badge widget for plan mode

Replace the plain text "PLAN MODE" indicator with a proper pill-shaped
badge using a new StatusBadge widget. The badge features rounded corners,
semi-transparent background, and theme-aware colors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Acrates/notedeck_dave/src/ui/badge.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_dave/src/ui/dave.rs | 13++++---------
Mcrates/notedeck_dave/src/ui/mod.rs | 1+
3 files changed, 156 insertions(+), 9 deletions(-)

diff --git a/crates/notedeck_dave/src/ui/badge.rs b/crates/notedeck_dave/src/ui/badge.rs @@ -0,0 +1,151 @@ +use egui::{Color32, Response, Ui, Vec2}; + +/// Badge variants that determine the color scheme +#[derive(Clone, Copy, Default)] +#[allow(dead_code)] +pub enum BadgeVariant { + /// Default muted style + #[default] + Default, + /// Informational blue + Info, + /// Success green + Success, + /// Warning amber/yellow + Warning, + /// Error/danger red + Destructive, +} + +impl BadgeVariant { + /// Get background and text colors for this variant + fn colors(&self, ui: &Ui) -> (Color32, Color32) { + let is_dark = ui.visuals().dark_mode; + + match self { + BadgeVariant::Default => { + let bg = if is_dark { + Color32::from_rgba_unmultiplied(255, 255, 255, 20) + } else { + Color32::from_rgba_unmultiplied(0, 0, 0, 15) + }; + let fg = ui.visuals().text_color(); + (bg, fg) + } + BadgeVariant::Info => { + // Blue tones + let bg = if is_dark { + Color32::from_rgba_unmultiplied(59, 130, 246, 30) + } else { + Color32::from_rgba_unmultiplied(59, 130, 246, 25) + }; + let fg = if is_dark { + Color32::from_rgb(147, 197, 253) // blue-300 + } else { + Color32::from_rgb(29, 78, 216) // blue-700 + }; + (bg, fg) + } + BadgeVariant::Success => { + // Green tones + let bg = if is_dark { + Color32::from_rgba_unmultiplied(34, 197, 94, 30) + } else { + Color32::from_rgba_unmultiplied(34, 197, 94, 25) + }; + let fg = if is_dark { + Color32::from_rgb(134, 239, 172) // green-300 + } else { + Color32::from_rgb(21, 128, 61) // green-700 + }; + (bg, fg) + } + BadgeVariant::Warning => { + // Amber/yellow tones + let bg = if is_dark { + Color32::from_rgba_unmultiplied(245, 158, 11, 30) + } else { + Color32::from_rgba_unmultiplied(245, 158, 11, 25) + }; + let fg = if is_dark { + Color32::from_rgb(252, 211, 77) // amber-300 + } else { + Color32::from_rgb(180, 83, 9) // amber-700 + }; + (bg, fg) + } + BadgeVariant::Destructive => { + // Red tones + let bg = if is_dark { + Color32::from_rgba_unmultiplied(239, 68, 68, 30) + } else { + Color32::from_rgba_unmultiplied(239, 68, 68, 25) + }; + let fg = if is_dark { + Color32::from_rgb(252, 165, 165) // red-300 + } else { + Color32::from_rgb(185, 28, 28) // red-700 + }; + (bg, fg) + } + } + } +} + +/// A pill-shaped status badge widget (shadcn-style) +pub struct StatusBadge<'a> { + text: &'a str, + variant: BadgeVariant, +} + +impl<'a> StatusBadge<'a> { + /// Create a new status badge with the given text + pub fn new(text: &'a str) -> Self { + Self { + text, + variant: BadgeVariant::Default, + } + } + + /// Set the badge variant + pub fn variant(mut self, variant: BadgeVariant) -> Self { + self.variant = variant; + self + } + + /// Show the badge and return the response + pub fn show(self, ui: &mut Ui) -> Response { + let (bg_color, text_color) = self.variant.colors(ui); + + // Calculate text size for proper allocation + let font_id = egui::FontId::proportional(11.0); + let galley = ui.painter().layout_no_wrap( + self.text.to_string(), + font_id.clone(), + text_color, + ); + + // Padding: horizontal 8px, vertical 2px + let padding = Vec2::new(8.0, 3.0); + let desired_size = galley.size() + padding * 2.0; + + let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover()); + + if ui.is_rect_visible(rect) { + let painter = ui.painter(); + + // Full pill rounding (half of height) + let rounding = rect.height() / 2.0; + + // Background + painter.rect_filled(rect, rounding, bg_color); + + // Text centered + let text_pos = rect.center() - galley.size() / 2.0; + painter.galley(text_pos, galley, text_color); + } + + response + } +} + diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -618,15 +618,10 @@ impl<'a> DaveUi<'a> { // Show plan mode indicator if self.plan_mode_active { - ui.add( - egui::Label::new( - egui::RichText::new("PLAN MODE") - .color(egui::Color32::from_rgb(100, 149, 237)) // Cornflower blue - .strong(), - ) - .selectable(false), - ) - .on_hover_text("Ctrl+P to toggle plan mode"); + super::badge::StatusBadge::new("PLAN") + .variant(super::badge::BadgeVariant::Info) + .show(ui) + .on_hover_text("Ctrl+P to toggle plan mode"); } let r = ui.add( diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -1,3 +1,4 @@ +pub mod badge; mod dave; pub mod diff; pub mod keybind_hint;