1use frus_core::{FontFamily, Point, Rect, ResolvedTextStyle, Scene, TextStyle};
4use frus_layout::{Dimension, Style};
5
6use crate::interaction::Status;
7use crate::theme::Theme;
8use crate::widget::Widget;
9
10const PAD_X: f32 = 7.0;
11const PAD_Y: f32 = 3.0;
12
13pub struct Kbd {
15 label: String,
16 text_style: Option<TextStyle>,
17}
18
19impl Kbd {
20 pub fn new(label: impl Into<String>) -> Self {
22 Self {
23 label: label.into(),
24 text_style: None,
25 }
26 }
27
28 #[must_use]
30 pub fn text_style(mut self, style: TextStyle) -> Self {
31 self.text_style = Some(style);
32 self
33 }
34
35 fn label_style(&self, theme: Option<&Theme>) -> ResolvedTextStyle {
44 self.text_style
45 .or(theme.and_then(|t| t.widgets.kbd.text_style))
46 .unwrap_or_else(|| {
47 crate::theme::type_scale(theme)
48 .label_medium
49 .family(FontFamily::Monospace)
50 })
51 .resolved()
52 }
53
54 fn sizing(&self, theme: Option<&Theme>) -> Style {
55 let measured = frus_text::measure_resolved(&self.label, &self.label_style(theme));
56 Style {
57 width: Dimension::Length((measured.width + PAD_X * 2.0).ceil()),
58 height: Dimension::Length((measured.height + PAD_Y * 2.0).ceil()),
59 ..Default::default()
60 }
61 }
62}
63
64impl<Msg> Widget<Msg> for Kbd {
65 fn style(&self) -> Style {
66 self.sizing(None)
67 }
68
69 fn style_themed(&self, theme: &Theme) -> Style {
70 self.sizing(Some(theme))
71 }
72
73 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
74 &[]
75 }
76
77 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
78 let o = status.opacity;
79 scene.draw_rect(
80 bounds,
81 theme.surface.fade(o),
82 5.0,
83 1.0,
84 theme.border.fade(o),
85 );
86 scene.text(
87 Point::new(bounds.x + PAD_X, bounds.y + PAD_Y),
88 self.label.clone(),
89 &self.label_style(Some(theme)),
90 theme.muted.fade(o),
91 );
92 }
93
94 fn on_click(&self) -> Option<Msg> {
95 None
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use frus_core::Primitive;
103
104 #[test]
105 fn paints_cap_and_label() {
106 let kbd = Kbd::new("Ctrl");
107 let mut scene = Scene::new();
108 Widget::<()>::paint(
109 &kbd,
110 Rect::new(0.0, 0.0, 40.0, 20.0),
111 Status::default(),
112 &Theme::default(),
113 &mut scene,
114 );
115 assert!(scene
116 .primitives()
117 .iter()
118 .any(|p| matches!(p, Primitive::Rect { border_width, .. } if *border_width > 0.0)));
119 assert!(scene
120 .primitives()
121 .iter()
122 .any(|p| matches!(p, Primitive::Text { text, .. } if text == "Ctrl")));
123 }
124}