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