commit bf4b34f2d409a0735aeb3d4dbba8903e1a377085
parent 1b300428e729f70e8270821b01bc2ce89065faf9
Author: William Casarin <jb55@jb55.com>
Date: Tue, 27 Jan 2026 11:06:43 -0800
dave: integrate keybind hints into Allow/Deny buttons
Add ActionButton widget with integrated keybind hints inside the pill,
replacing the separate keybind_hint() calls next to the buttons. This
matches the StatusBadge pattern for showing keyboard shortcuts inline.
Also rename "Will Accept" to "Will Allow" for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
2 files changed, 122 insertions(+), 23 deletions(-)
diff --git a/crates/notedeck_dave/src/ui/badge.rs b/crates/notedeck_dave/src/ui/badge.rs
@@ -205,3 +205,104 @@ impl<'a> StatusBadge<'a> {
}
}
+/// A pill-shaped action button with integrated keybind hint
+pub struct ActionButton<'a> {
+ text: &'a str,
+ bg_color: Color32,
+ text_color: Color32,
+ keybind: Option<&'a str>,
+}
+
+impl<'a> ActionButton<'a> {
+ /// Create a new action button with the given text and colors
+ pub fn new(text: &'a str, bg_color: Color32, text_color: Color32) -> Self {
+ Self {
+ text,
+ bg_color,
+ text_color,
+ keybind: None,
+ }
+ }
+
+ /// Add a keybind hint inside the button (e.g., "1" for key 1)
+ pub fn keybind(mut self, key: &'a str) -> Self {
+ self.keybind = Some(key);
+ self
+ }
+
+ /// Show the button and return the response
+ pub fn show(self, ui: &mut Ui) -> Response {
+ // Calculate text size for proper allocation
+ let font_id = egui::FontId::proportional(13.0);
+ let galley = ui.painter().layout_no_wrap(
+ self.text.to_string(),
+ font_id.clone(),
+ self.text_color,
+ );
+
+ // Calculate keybind box size if present
+ let keybind_box_size = 16.0;
+ let keybind_spacing = 6.0;
+ let keybind_extra = if self.keybind.is_some() {
+ keybind_box_size + keybind_spacing
+ } else {
+ 0.0
+ };
+
+ // Padding: horizontal 10px, vertical 4px
+ let padding = Vec2::new(10.0, 4.0);
+ let desired_size =
+ Vec2::new(galley.size().x + keybind_extra, galley.size().y) + padding * 2.0;
+
+ let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
+
+ if ui.is_rect_visible(rect) {
+ let painter = ui.painter();
+
+ // Adjust color based on hover/click state
+ let bg_color = if response.is_pointer_button_down_on() {
+ self.bg_color.gamma_multiply(0.8)
+ } else if response.hovered() {
+ self.bg_color.gamma_multiply(1.15)
+ } else {
+ self.bg_color
+ };
+
+ // Full pill rounding (half of height)
+ let rounding = rect.height() / 2.0;
+
+ // Background
+ painter.rect_filled(rect, rounding, bg_color);
+
+ // Text (offset left if keybind present)
+ let text_offset_x = if self.keybind.is_some() {
+ -keybind_extra / 2.0
+ } else {
+ 0.0
+ };
+ let text_pos =
+ rect.center() + Vec2::new(text_offset_x, 0.0) - galley.size() / 2.0;
+ painter.galley(text_pos, galley, self.text_color);
+
+ // Draw keybind hint if present (no background, just text)
+ if let Some(key) = self.keybind {
+ let key_center = egui::pos2(
+ rect.right() - padding.x - keybind_box_size / 2.0,
+ rect.center().y,
+ );
+
+ // Keybind text using the button's text color
+ painter.text(
+ key_center,
+ egui::Align2::CENTER_CENTER,
+ key,
+ egui::FontId::monospace(keybind_box_size * 0.7),
+ self.text_color,
+ );
+ }
+ }
+
+ response
+ }
+}
+
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -429,17 +429,17 @@ impl<'a> DaveUi<'a> {
let shift_held = ui.input(|i| i.modifiers.shift);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
- // Deny button (red) with keybind hint on left
- let deny_response = ui
- .add(
- egui::Button::new(
- egui::RichText::new("Deny")
- .color(ui.visuals().widgets.active.fg_stroke.color),
- )
- .fill(egui::Color32::from_rgb(178, 34, 34)),
- )
- .on_hover_text("Press 2 to deny, Shift+2 to deny with message");
- super::keybind_hint(ui, "2");
+ let button_text_color = ui.visuals().widgets.active.fg_stroke.color;
+
+ // Deny button (red) with integrated keybind hint
+ let deny_response = super::badge::ActionButton::new(
+ "Deny",
+ egui::Color32::from_rgb(178, 34, 34),
+ button_text_color,
+ )
+ .keybind("2")
+ .show(ui)
+ .on_hover_text("Press 2 to deny, Shift+2 to deny with message");
if deny_response.clicked() {
if shift_held {
@@ -456,17 +456,15 @@ impl<'a> DaveUi<'a> {
}
}
- // Allow button (green) with keybind hint on left
- let allow_response = ui
- .add(
- egui::Button::new(
- egui::RichText::new("Allow")
- .color(ui.visuals().widgets.active.fg_stroke.color),
- )
- .fill(egui::Color32::from_rgb(34, 139, 34)),
- )
- .on_hover_text("Press 1 to allow, Shift+1 to allow with message");
- super::keybind_hint(ui, "1");
+ // Allow button (green) with integrated keybind hint
+ let allow_response = super::badge::ActionButton::new(
+ "Allow",
+ egui::Color32::from_rgb(34, 139, 34),
+ button_text_color,
+ )
+ .keybind("1")
+ .show(ui)
+ .on_hover_text("Press 1 to allow, Shift+1 to allow with message");
if allow_response.clicked() {
if shift_held {
@@ -485,7 +483,7 @@ impl<'a> DaveUi<'a> {
match self.permission_message_state {
PermissionMessageState::TentativeAccept => {
ui.label(
- egui::RichText::new("✓ Will Accept")
+ egui::RichText::new("✓ Will Allow")
.color(egui::Color32::from_rgb(100, 180, 100))
.strong(),
);