revue/widget/input_widgets/
button.rs1use crate::event::{Key, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
4use crate::layout::Rect;
5use crate::render::Cell;
6use crate::style::Color;
7use crate::widget::traits::{
8 EventResult, Interactive, RenderContext, View, WidgetProps, WidgetState,
9};
10use crate::{impl_styled_view, impl_widget_builders};
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum ButtonVariant {
15 #[default]
17 Default,
18 Primary,
20 Danger,
22 Ghost,
24 Success,
26}
27
28#[derive(Clone, Debug)]
30pub struct Button {
31 label: String,
32 icon: Option<char>,
34 variant: ButtonVariant,
35 state: WidgetState,
37 props: WidgetProps,
39 width: Option<u16>,
40}
41
42impl Button {
43 pub fn new(label: impl Into<String>) -> Self {
45 Self {
46 label: label.into(),
47 icon: None,
48 variant: ButtonVariant::Default,
49 state: WidgetState::new(),
50 props: WidgetProps::new(),
51 width: None,
52 }
53 }
54
55 pub fn icon(mut self, icon: char) -> Self {
71 self.icon = Some(icon);
72 self
73 }
74
75 pub fn primary(label: impl Into<String>) -> Self {
77 Self::new(label).variant(ButtonVariant::Primary)
78 }
79
80 pub fn danger(label: impl Into<String>) -> Self {
82 Self::new(label).variant(ButtonVariant::Danger)
83 }
84
85 pub fn ghost(label: impl Into<String>) -> Self {
87 Self::new(label).variant(ButtonVariant::Ghost)
88 }
89
90 pub fn success(label: impl Into<String>) -> Self {
92 Self::new(label).variant(ButtonVariant::Success)
93 }
94
95 pub fn variant(mut self, variant: ButtonVariant) -> Self {
97 self.variant = variant;
98 self
99 }
100
101 pub fn width(mut self, width: u16) -> Self {
103 self.width = Some(width);
104 self
105 }
106
107 pub fn handle_key(&mut self, key: &Key) -> bool {
109 if self.state.disabled {
110 return false;
111 }
112
113 matches!(key, Key::Enter | Key::Char(' '))
114 }
115
116 pub fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> (bool, bool) {
128 if self.state.disabled {
129 return (false, false);
130 }
131
132 let inside = area.contains(event.x, event.y);
133 let mut needs_render = false;
134 let mut was_clicked = false;
135
136 match event.kind {
137 MouseEventKind::Down(MouseButton::Left) if inside => {
138 if !self.state.pressed {
139 self.state.pressed = true;
140 needs_render = true;
141 }
142 }
143 MouseEventKind::Up(MouseButton::Left) => {
144 if self.state.pressed {
145 self.state.pressed = false;
146 needs_render = true;
147 if inside {
148 was_clicked = true;
149 }
150 }
151 }
152 MouseEventKind::Move => {
153 let was_hovered = self.state.hovered;
154 self.state.hovered = inside;
155 if was_hovered != self.state.hovered {
156 needs_render = true;
157 }
158 }
159 _ => {}
160 }
161
162 (needs_render, was_clicked)
163 }
164
165 pub fn is_pressed(&self) -> bool {
167 self.state.is_pressed()
168 }
169
170 pub fn is_hovered(&self) -> bool {
172 self.state.is_hovered()
173 }
174
175 fn get_variant_base_colors(&self) -> (Color, Color) {
177 match self.variant {
178 ButtonVariant::Default => (Color::WHITE, Color::rgb(60, 60, 60)),
179 ButtonVariant::Primary => (Color::WHITE, Color::rgb(37, 99, 235)),
180 ButtonVariant::Danger => (Color::WHITE, Color::rgb(220, 38, 38)),
181 ButtonVariant::Ghost => (Color::rgb(200, 200, 200), Color::rgb(30, 30, 30)),
182 ButtonVariant::Success => (Color::WHITE, Color::rgb(22, 163, 74)),
183 }
184 }
185
186 fn get_colors_from_ctx(&self, ctx: &RenderContext) -> (Color, Color) {
195 let (variant_fg, variant_bg) = self.get_variant_base_colors();
196 self.state
197 .resolve_colors_interactive(ctx.style, variant_fg, variant_bg)
198 }
199}
200
201impl Default for Button {
202 fn default() -> Self {
203 Self::new("")
204 }
205}
206
207impl View for Button {
208 fn render(&self, ctx: &mut RenderContext) {
209 let area = ctx.area;
210 if area.width == 0 || area.height == 0 {
211 return;
212 }
213
214 let (fg, bg) = self.get_colors_from_ctx(ctx);
216
217 let icon_width = if self.icon.is_some() { 2u16 } else { 0 }; let label_width = self.label.chars().count() as u16;
220 let content_width = icon_width + label_width;
221 let padding = 2; let min_width = self.width.unwrap_or(0);
223 let button_width = (content_width + padding * 2).max(min_width).min(area.width);
224
225 for x in 0..button_width {
227 let mut cell = Cell::new(' ');
228 cell.bg = Some(bg);
229 ctx.buffer.set(area.x + x, area.y, cell);
230 }
231
232 let content_start = (button_width.saturating_sub(content_width)) / 2;
234 let mut x = area.x + content_start;
235
236 if let Some(icon) = self.icon {
238 if x < area.x + button_width {
239 let mut cell = Cell::new(icon);
240 cell.fg = Some(fg);
241 cell.bg = Some(bg);
242 if self.state.focused && !self.state.disabled {
243 cell.modifier = crate::render::Modifier::BOLD;
244 }
245 ctx.buffer.set(x, area.y, cell);
246 x += 1;
247
248 if x < area.x + button_width {
250 let mut space = Cell::new(' ');
251 space.bg = Some(bg);
252 ctx.buffer.set(x, area.y, space);
253 x += 1;
254 }
255 }
256 }
257
258 if self.state.focused && !self.state.disabled {
260 ctx.draw_text_bg_bold(x, area.y, &self.label, fg, bg);
261 } else {
262 ctx.draw_text_bg(x, area.y, &self.label, fg, bg);
263 }
264
265 if self.state.focused && !self.state.disabled {
267 if area.x > 0 {
269 let mut left = Cell::new('[');
270 left.fg = Some(Color::CYAN);
271 ctx.buffer.set(area.x.saturating_sub(1), area.y, left);
272 }
273
274 let right_x = area.x + button_width;
275 if right_x < area.x + area.width {
276 let mut right = Cell::new(']');
277 right.fg = Some(Color::CYAN);
278 ctx.buffer.set(right_x, area.y, right);
279 }
280 }
281 }
282
283 crate::impl_view_meta!("Button");
284}
285
286impl Interactive for Button {
287 fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
288 if self.state.disabled {
289 return EventResult::Ignored;
290 }
291
292 match event.key {
293 Key::Enter | Key::Char(' ') => EventResult::ConsumedAndRender,
294 _ => EventResult::Ignored,
295 }
296 }
297
298 fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> EventResult {
299 if self.state.disabled {
300 return EventResult::Ignored;
301 }
302
303 let inside = area.contains(event.x, event.y);
304
305 match event.kind {
306 MouseEventKind::Down(MouseButton::Left) if inside => {
307 if !self.state.pressed {
308 self.state.pressed = true;
309 return EventResult::ConsumedAndRender;
310 }
311 EventResult::Consumed
312 }
313 MouseEventKind::Up(MouseButton::Left) => {
314 if self.state.pressed {
315 self.state.pressed = false;
316 return if inside {
318 EventResult::ConsumedAndRender
319 } else {
320 EventResult::Consumed
321 };
322 }
323 EventResult::Ignored
324 }
325 MouseEventKind::Move => {
326 let was_hovered = self.state.hovered;
327 self.state.hovered = inside;
328 if was_hovered != self.state.hovered {
329 EventResult::ConsumedAndRender
330 } else {
331 EventResult::Ignored
332 }
333 }
334 _ => EventResult::Ignored,
335 }
336 }
337
338 fn focusable(&self) -> bool {
339 !self.state.disabled
340 }
341
342 fn on_focus(&mut self) {
343 self.state.focused = true;
344 }
345
346 fn on_blur(&mut self) {
347 self.state.focused = false;
348 self.state.reset_transient();
349 }
350}
351
352pub fn button(label: impl Into<String>) -> Button {
354 Button::new(label)
355}
356
357impl_styled_view!(Button);
358impl_widget_builders!(Button);
359
360#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_button_new() {
369 let btn = Button::new("Click");
370 assert_eq!(btn.label, "Click");
371 assert!(!btn.is_focused());
372 assert!(!btn.is_disabled());
373 }
374
375 #[test]
376 fn test_button_variants() {
377 let primary = Button::primary("Primary");
378 assert_eq!(primary.variant, ButtonVariant::Primary);
379
380 let danger = Button::danger("Danger");
381 assert_eq!(danger.variant, ButtonVariant::Danger);
382
383 let ghost = Button::ghost("Ghost");
384 assert_eq!(ghost.variant, ButtonVariant::Ghost);
385
386 let success = Button::success("Success");
387 assert_eq!(success.variant, ButtonVariant::Success);
388 }
389
390 #[test]
391 fn test_button_builder() {
392 let btn = Button::new("Test")
393 .variant(ButtonVariant::Primary)
394 .focused(true)
395 .disabled(false)
396 .width(20);
397
398 assert_eq!(btn.variant, ButtonVariant::Primary);
399 assert!(btn.is_focused());
400 assert!(!btn.is_disabled());
401 assert_eq!(btn.width, Some(20));
402 }
403
404 #[test]
405 fn test_button_handle_key() {
406 let mut btn = Button::new("Test");
407
408 assert!(btn.handle_key(&Key::Enter));
409 assert!(btn.handle_key(&Key::Char(' ')));
410 assert!(!btn.handle_key(&Key::Char('a')));
411
412 btn.state.disabled = true;
413 assert!(!btn.handle_key(&Key::Enter));
414 }
415
416 #[test]
417 fn test_button_helper() {
418 let btn = button("Helper");
419 assert_eq!(btn.label, "Helper");
420 }
421
422 #[test]
423 fn test_button_custom_colors() {
424 let btn = Button::new("Custom").fg(Color::RED).bg(Color::BLUE);
425
426 assert_eq!(btn.state.fg, Some(Color::RED));
427 assert_eq!(btn.state.bg, Some(Color::BLUE));
428 }
429
430 #[test]
431 fn test_button_with_icon() {
432 let btn = Button::new("Save").icon('💾');
433 assert_eq!(btn.icon, Some('💾'));
434 assert_eq!(btn.label, "Save");
435 }
436
437 #[test]
438 fn test_button_icon_width() {
439 let btn_no_icon = Button::new("OK");
440 let btn_with_icon = Button::new("OK").icon('✓');
441
442 assert!(btn_with_icon.icon.is_some());
443 assert!(btn_no_icon.icon.is_none());
444 }
445}