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