rat_widget/
button.rs

1//!
2//! Button widget.
3//!
4//! Render:
5//! ```rust ignore
6//! Button::new("Button")
7//!      .styles(THEME.button_style()) //
8//!      .render(b_area_1, frame.buffer_mut(), &mut state.button1);
9//! ```
10//!
11//! Event handling:
12//! ```rust ignore
13//! match state.button1.handle(event, Regular) {
14//!     ButtonOutcome::Pressed => {
15//!         data.p1 += 1;
16//!         Outcome::Changed
17//!     }
18//!     r => r.into(),
19//! }
20//! ```
21//!
22
23use crate::_private::NonExhaustive;
24use crate::button::event::ButtonOutcome;
25use crate::util::{block_size, revert_style};
26use rat_event::util::{have_keyboard_enhancement, MouseFlags};
27use rat_event::{ct_event, ConsumedEvent, HandleEvent, MouseOnly, Regular};
28use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
29use rat_reloc::{relocate_area, RelocatableState};
30use ratatui::buffer::Buffer;
31use ratatui::layout::Rect;
32use ratatui::prelude::BlockExt;
33use ratatui::style::Style;
34use ratatui::text::Text;
35#[cfg(feature = "unstable-widget-ref")]
36use ratatui::widgets::StatefulWidgetRef;
37use ratatui::widgets::{Block, StatefulWidget, Widget};
38use std::thread;
39use std::time::Duration;
40
41/// Button widget.
42#[derive(Debug, Default, Clone)]
43pub struct Button<'a> {
44    text: Text<'a>,
45    style: Style,
46    focus_style: Option<Style>,
47    hover_style: Option<Style>,
48    armed_style: Option<Style>,
49    armed_delay: Option<Duration>,
50    block: Option<Block<'a>>,
51}
52
53/// Composite style.
54#[derive(Debug, Clone)]
55pub struct ButtonStyle {
56    /// Base style
57    pub style: Style,
58    /// Focused style
59    pub focus: Option<Style>,
60    /// Armed style
61    pub armed: Option<Style>,
62    /// Hover style
63    pub hover: Option<Style>,
64    /// Button border
65    pub block: Option<Block<'static>>,
66    /// Some terminals repaint too fast to see the click.
67    /// This adds some delay when the button state goes from
68    /// armed to clicked.
69    pub armed_delay: Option<Duration>,
70
71    pub non_exhaustive: NonExhaustive,
72}
73
74/// State & event-handling.
75#[derive(Debug)]
76pub struct ButtonState {
77    /// Complete area
78    /// __readonly__. renewed for each render.
79    pub area: Rect,
80    /// Area inside the block.
81    /// __readonly__. renewed for each render.
82    pub inner: Rect,
83
84    /// Hover is enabled?
85    pub hover_enabled: bool,
86    /// Button has been clicked but not released yet.
87    /// __used for mouse interaction__
88    pub armed: bool,
89    /// Some terminals repaint too fast to see the click.
90    /// This adds some delay when the button state goes from
91    /// armed to clicked.
92    ///
93    /// Default is 50ms.
94    pub armed_delay: Option<Duration>,
95
96    /// Current focus state.
97    /// __read+write__
98    pub focus: FocusFlag,
99
100    pub mouse: MouseFlags,
101
102    pub non_exhaustive: NonExhaustive,
103}
104
105impl Default for ButtonStyle {
106    fn default() -> Self {
107        Self {
108            style: Default::default(),
109            focus: None,
110            armed: None,
111            hover: None,
112            block: None,
113            armed_delay: None,
114            non_exhaustive: NonExhaustive,
115        }
116    }
117}
118
119impl<'a> Button<'a> {
120    pub fn new(text: impl Into<Text<'a>>) -> Self {
121        Self::default().text(text)
122    }
123
124    /// Set all styles.
125    #[inline]
126    pub fn styles_opt(self, styles: Option<ButtonStyle>) -> Self {
127        if let Some(styles) = styles {
128            self.styles(styles)
129        } else {
130            self
131        }
132    }
133
134    /// Set all styles.
135    #[inline]
136    pub fn styles(mut self, styles: ButtonStyle) -> Self {
137        self.style = styles.style;
138        if styles.focus.is_some() {
139            self.focus_style = styles.focus;
140        }
141        if styles.armed.is_some() {
142            self.armed_style = styles.armed;
143        }
144        if styles.armed_delay.is_some() {
145            self.armed_delay = styles.armed_delay;
146        }
147        if styles.hover.is_some() {
148            self.hover_style = styles.hover;
149        }
150        if let Some(block) = styles.block {
151            self.block = Some(block);
152        }
153        self.block = self.block.map(|v| v.style(self.style));
154        self
155    }
156
157    /// Set the base-style.
158    #[inline]
159    pub fn style(mut self, style: impl Into<Style>) -> Self {
160        self.style = style.into();
161        self
162    }
163
164    /// Style when focused.
165    #[inline]
166    pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
167        self.focus_style = Some(style.into());
168        self
169    }
170
171    /// Style when clicked but not released.
172    #[inline]
173    pub fn armed_style(mut self, style: impl Into<Style>) -> Self {
174        self.armed_style = Some(style.into());
175        self
176    }
177
178    /// Some terminals repaint too fast to see the click.
179    /// This adds some delay when the button state goes from
180    /// armed to clicked.
181    pub fn armed_delay(mut self, delay: Duration) -> Self {
182        self.armed_delay = Some(delay);
183        self
184    }
185
186    /// Style for hover over the button.
187    pub fn hover_style(mut self, style: impl Into<Style>) -> Self {
188        self.hover_style = Some(style.into());
189        self
190    }
191
192    /// Button text.
193    #[inline]
194    pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
195        self.text = text.into().centered();
196        self
197    }
198
199    /// Left align button text.
200    pub fn left_aligned(mut self) -> Self {
201        self.text = self.text.left_aligned();
202        self
203    }
204
205    /// Right align button text.
206    pub fn right_aligned(mut self) -> Self {
207        self.text = self.text.right_aligned();
208        self
209    }
210
211    /// Block.
212    #[inline]
213    pub fn block(mut self, block: Block<'a>) -> Self {
214        self.block = Some(block);
215        self.block = self.block.map(|v| v.style(self.style));
216        self
217    }
218
219    /// Inherent width.
220    pub fn width(&self) -> u16 {
221        self.text.width() as u16 + block_size(&self.block).width
222    }
223
224    /// Inherent height.
225    pub fn height(&self) -> u16 {
226        self.text.height() as u16 + block_size(&self.block).height
227    }
228}
229
230#[cfg(feature = "unstable-widget-ref")]
231impl<'a> StatefulWidgetRef for Button<'a> {
232    type State = ButtonState;
233
234    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
235        render_ref(self, area, buf, state);
236    }
237}
238
239impl StatefulWidget for Button<'_> {
240    type State = ButtonState;
241
242    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
243        render_ref(&self, area, buf, state);
244    }
245}
246
247fn render_ref(widget: &Button<'_>, area: Rect, buf: &mut Buffer, state: &mut ButtonState) {
248    state.area = area;
249    state.inner = widget.block.inner_if_some(area);
250    state.armed_delay = widget.armed_delay;
251    state.hover_enabled = widget.hover_style.is_some();
252
253    let style = widget.style;
254    let focus_style = if let Some(focus_style) = widget.focus_style {
255        focus_style
256    } else {
257        revert_style(style)
258    };
259    let armed_style = if let Some(armed_style) = widget.armed_style {
260        armed_style
261    } else {
262        if state.is_focused() {
263            revert_style(focus_style)
264        } else {
265            revert_style(style)
266        }
267    };
268
269    if widget.block.is_some() {
270        widget.block.render(area, buf);
271    } else {
272        buf.set_style(area, style);
273    }
274
275    if state.mouse.hover.get() && widget.hover_style.is_some() {
276        buf.set_style(state.inner, widget.hover_style.expect("style"))
277    } else if state.is_focused() {
278        buf.set_style(state.inner, focus_style);
279    }
280
281    if state.armed {
282        let armed_area = Rect::new(
283            state.inner.x + 1,
284            state.inner.y,
285            state.inner.width.saturating_sub(2),
286            state.inner.height,
287        );
288        buf.set_style(armed_area, style.patch(armed_style));
289    }
290
291    let h = widget.text.height() as u16;
292    let r = state.inner.height.saturating_sub(h) / 2;
293    let area = Rect::new(state.inner.x, state.inner.y + r, state.inner.width, h);
294    (&widget.text).render(area, buf);
295}
296
297impl Clone for ButtonState {
298    fn clone(&self) -> Self {
299        Self {
300            area: self.area,
301            inner: self.inner,
302            hover_enabled: false,
303            armed: self.armed,
304            armed_delay: self.armed_delay,
305            focus: FocusFlag::named(self.focus.name()),
306            mouse: Default::default(),
307            non_exhaustive: NonExhaustive,
308        }
309    }
310}
311
312impl Default for ButtonState {
313    fn default() -> Self {
314        Self {
315            area: Default::default(),
316            inner: Default::default(),
317            hover_enabled: false,
318            armed: false,
319            armed_delay: None,
320            focus: Default::default(),
321            mouse: Default::default(),
322            non_exhaustive: NonExhaustive,
323        }
324    }
325}
326
327impl ButtonState {
328    pub fn new() -> Self {
329        Self::default()
330    }
331
332    pub fn named(name: &str) -> Self {
333        Self {
334            focus: FocusFlag::named(name),
335            ..Default::default()
336        }
337    }
338}
339
340impl HasFocus for ButtonState {
341    fn build(&self, builder: &mut FocusBuilder) {
342        builder.leaf_widget(self);
343    }
344
345    #[inline]
346    fn focus(&self) -> FocusFlag {
347        self.focus.clone()
348    }
349
350    #[inline]
351    fn area(&self) -> Rect {
352        self.area
353    }
354}
355
356impl RelocatableState for ButtonState {
357    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
358        self.area = relocate_area(self.area, shift, clip);
359        self.inner = relocate_area(self.inner, shift, clip);
360    }
361}
362
363pub(crate) mod event {
364    use rat_event::{ConsumedEvent, Outcome};
365
366    /// Result value for event-handling.
367    ///
368    /// Adds `Pressed` to the general Outcome.
369    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
370    pub enum ButtonOutcome {
371        /// The given event was not handled at all.
372        Continue,
373        /// The event was handled, no repaint necessary.
374        Unchanged,
375        /// The event was handled, repaint necessary.
376        Changed,
377        /// Button has been pressed.
378        Pressed,
379    }
380
381    impl ConsumedEvent for ButtonOutcome {
382        fn is_consumed(&self) -> bool {
383            *self != ButtonOutcome::Continue
384        }
385    }
386
387    impl From<ButtonOutcome> for Outcome {
388        fn from(value: ButtonOutcome) -> Self {
389            match value {
390                ButtonOutcome::Continue => Outcome::Continue,
391                ButtonOutcome::Unchanged => Outcome::Unchanged,
392                ButtonOutcome::Changed => Outcome::Changed,
393                ButtonOutcome::Pressed => Outcome::Changed,
394            }
395        }
396    }
397}
398
399impl HandleEvent<crossterm::event::Event, Regular, ButtonOutcome> for ButtonState {
400    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> ButtonOutcome {
401        let r = if self.is_focused() {
402            // Release keys may not be available.
403            if have_keyboard_enhancement() {
404                match event {
405                    ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
406                        self.armed = true;
407                        ButtonOutcome::Changed
408                    }
409                    ct_event!(keycode release Enter) | ct_event!(key release ' ') => {
410                        if self.armed {
411                            if let Some(delay) = self.armed_delay {
412                                thread::sleep(delay);
413                            }
414                            self.armed = false;
415                            ButtonOutcome::Pressed
416                        } else {
417                            // single key release happen more often than not.
418                            ButtonOutcome::Unchanged
419                        }
420                    }
421                    _ => ButtonOutcome::Continue,
422                }
423            } else {
424                match event {
425                    ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
426                        ButtonOutcome::Pressed
427                    }
428                    _ => ButtonOutcome::Continue,
429                }
430            }
431        } else {
432            ButtonOutcome::Continue
433        };
434
435        if r == ButtonOutcome::Continue {
436            HandleEvent::handle(self, event, MouseOnly)
437        } else {
438            r
439        }
440    }
441}
442
443impl HandleEvent<crossterm::event::Event, MouseOnly, ButtonOutcome> for ButtonState {
444    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> ButtonOutcome {
445        match event {
446            ct_event!(mouse down Left for column, row) => {
447                if self.area.contains((*column, *row).into()) {
448                    self.armed = true;
449                    ButtonOutcome::Changed
450                } else {
451                    ButtonOutcome::Continue
452                }
453            }
454            ct_event!(mouse up Left for column, row) => {
455                if self.area.contains((*column, *row).into()) {
456                    if self.armed {
457                        self.armed = false;
458                        ButtonOutcome::Pressed
459                    } else {
460                        ButtonOutcome::Continue
461                    }
462                } else {
463                    if self.armed {
464                        self.armed = false;
465                        ButtonOutcome::Changed
466                    } else {
467                        ButtonOutcome::Continue
468                    }
469                }
470            }
471            ct_event!(mouse any for m) if self.mouse.hover(self.area, m) => ButtonOutcome::Changed,
472            _ => ButtonOutcome::Continue,
473        }
474    }
475}
476
477/// Check event-handling for this hot-key and do Regular key-events otherwise.
478impl HandleEvent<crossterm::event::Event, crossterm::event::KeyEvent, ButtonOutcome>
479    for ButtonState
480{
481    fn handle(
482        &mut self,
483        event: &crossterm::event::Event,
484        hotkey: crossterm::event::KeyEvent,
485    ) -> ButtonOutcome {
486        use crossterm::event::Event;
487
488        let r = match event {
489            Event::Key(key) => {
490                // Release keys may not be available.
491                if have_keyboard_enhancement() {
492                    if hotkey.code == key.code && hotkey.modifiers == key.modifiers {
493                        if key.kind == crossterm::event::KeyEventKind::Press {
494                            self.armed = true;
495                            ButtonOutcome::Changed
496                        } else if key.kind == crossterm::event::KeyEventKind::Release {
497                            if self.armed {
498                                if let Some(delay) = self.armed_delay {
499                                    thread::sleep(delay);
500                                }
501                                self.armed = false;
502                                ButtonOutcome::Pressed
503                            } else {
504                                // single key release happen more often than not.
505                                ButtonOutcome::Unchanged
506                            }
507                        } else {
508                            ButtonOutcome::Continue
509                        }
510                    } else {
511                        ButtonOutcome::Continue
512                    }
513                } else {
514                    if hotkey.code == key.code && hotkey.modifiers == key.modifiers {
515                        if key.kind == crossterm::event::KeyEventKind::Press {
516                            ButtonOutcome::Pressed
517                        } else {
518                            ButtonOutcome::Continue
519                        }
520                    } else {
521                        ButtonOutcome::Continue
522                    }
523                }
524            }
525            _ => ButtonOutcome::Continue,
526        };
527
528        r.or_else(|| self.handle(event, Regular))
529    }
530}
531
532/// Handle all events.
533/// Text events are only processed if focus is true.
534/// Mouse events are processed if they are in range.
535pub fn handle_events(
536    state: &mut ButtonState,
537    focus: bool,
538    event: &crossterm::event::Event,
539) -> ButtonOutcome {
540    state.focus.set(focus);
541    HandleEvent::handle(state, event, Regular)
542}
543
544/// Handle only mouse-events.
545pub fn handle_mouse_events(
546    state: &mut ButtonState,
547    event: &crossterm::event::Event,
548) -> ButtonOutcome {
549    HandleEvent::handle(state, event, MouseOnly)
550}