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