1use sequoia_core::{Color, Edges, Length, Size};
2use sequoia_layout::{LayoutCtx, LayoutStyle};
3use sequoia_style::KbdStyle;
4use sequoia_view::{FontWeight, PaintCtx, Unit, View};
5
6const PAD_X: f32 = 8.0;
7const PAD_Y: f32 = 4.0;
8const RADIUS: f32 = 6.0;
9const FONT_SIZE: f32 = 12.0;
10const BORDER_W: f32 = 1.0;
11
12pub struct Kbd {
13 label: String,
14 style: Option<KbdStyle>,
15 background: Option<Color>,
16 border: Option<Color>,
17 text_color: Option<Color>,
18 font_size: Option<f32>,
19 radius: Option<f32>,
20 padding: Option<(f32, f32)>,
21 margin: Edges,
22}
23
24impl Kbd {
25 pub fn new(label: impl Into<String>) -> Self {
26 Self {
27 label: label.into(),
28 style: None,
29 background: None,
30 border: None,
31 text_color: None,
32 font_size: None,
33 radius: None,
34 padding: None,
35 margin: Edges::ZERO,
36 }
37 }
38
39 pub fn style(mut self, style: KbdStyle) -> Self {
40 self.style = Some(style);
41 self
42 }
43
44 pub fn background(mut self, color: Color) -> Self {
45 self.background = Some(color);
46 self
47 }
48
49 pub fn border(mut self, color: Color) -> Self {
50 self.border = Some(color);
51 self
52 }
53
54 pub fn text_color(mut self, color: Color) -> Self {
55 self.text_color = Some(color);
56 self
57 }
58
59 pub fn font_size(mut self, size: f32) -> Self {
60 self.font_size = Some(size);
61 self
62 }
63
64 pub fn radius(mut self, radius: f32) -> Self {
65 self.radius = Some(radius);
66 self
67 }
68
69 pub fn padding_xy(mut self, pad_x: f32, pad_y: f32) -> Self {
70 self.padding = Some((pad_x, pad_y));
71 self
72 }
73
74 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
75 let m = margin.into();
76 self.margin = Edges::only(
77 sanitize_margin(m.left),
78 sanitize_margin(m.top),
79 sanitize_margin(m.right),
80 sanitize_margin(m.bottom),
81 );
82 self
83 }
84
85 pub fn margin_xy(self, x: f32, y: f32) -> Self {
86 self.margin(Edges::xy(x, y))
87 }
88
89 pub fn margin_x(mut self, x: f32) -> Self {
90 let x = sanitize_margin(x);
91 self.margin.left = x;
92 self.margin.right = x;
93 self
94 }
95
96 pub fn margin_y(mut self, y: f32) -> Self {
97 let y = sanitize_margin(y);
98 self.margin.top = y;
99 self.margin.bottom = y;
100 self
101 }
102
103 pub fn margin_left(mut self, v: f32) -> Self {
104 self.margin.left = sanitize_margin(v);
105 self
106 }
107
108 pub fn margin_right(mut self, v: f32) -> Self {
109 self.margin.right = sanitize_margin(v);
110 self
111 }
112
113 pub fn margin_top(mut self, v: f32) -> Self {
114 self.margin.top = sanitize_margin(v);
115 self
116 }
117
118 pub fn margin_bottom(mut self, v: f32) -> Self {
119 self.margin.bottom = sanitize_margin(v);
120 self
121 }
122
123 fn font_size_resolved(&self, style: KbdStyle) -> f32 {
124 self.font_size.or(style.font_size).unwrap_or(FONT_SIZE)
125 }
126
127 fn padding_resolved(&self, style: KbdStyle) -> (f32, f32) {
128 self.padding.or(style.padding).unwrap_or((PAD_X, PAD_Y))
129 }
130}
131
132fn sanitize_margin(v: f32) -> f32 {
133 if v.is_finite() { v.max(0.0) } else { 0.0 }
134}
135
136impl View for Kbd {
137 fn body(&self) -> impl View + 'static {
138 Unit
139 }
140 fn is_leaf(&self) -> bool {
141 true
142 }
143 fn layout_style(&self) -> LayoutStyle {
144 LayoutStyle {
145 width: Length::Fit,
146 height: Length::Fit,
147 margin: self.margin,
148 ..LayoutStyle::default()
149 }
150 }
151 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
152 let style = self.style.unwrap_or_default();
153 let size = self.font_size_resolved(style);
154 let (pad_x, pad_y) = self.padding_resolved(style);
155 let w = ctx.text_width_weighted(&self.label, size, FontWeight::SEMIBOLD);
156 Some(Size::new(
157 w + pad_x * 2.0,
158 LayoutCtx::text_line_height(size) + pad_y * 2.0,
159 ))
160 }
161 fn paint_self(&self, ctx: &mut PaintCtx) {
162 if self.label.is_empty() {
163 return;
164 }
165 let rect = ctx.rect;
166 if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
167 return;
168 }
169 let theme = ctx.theme();
170 let style = self.style.unwrap_or_default();
171 let size = self.font_size_resolved(style);
172 let (pad_x, _) = self.padding_resolved(style);
173 let background = self
174 .background
175 .or(style.background)
176 .unwrap_or(theme.surface);
177 let border = self.border.or(style.border).unwrap_or(theme.border);
178 let text = self.text_color.or(style.text).unwrap_or(theme.text);
179 let radius = self.radius.or(style.radius).unwrap_or(RADIUS);
180 ctx.bordered_quad(rect, background, radius, BORDER_W, border);
181 let x = rect.left() + pad_x;
182 let y = rect.top() + rect.size.height * 0.5 + size * 0.35;
183 ctx.draw_text_weighted(&self.label, (x, y), size, FontWeight::SEMIBOLD, text);
184 }
185}