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