1use 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#[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#[derive(Debug, Clone)]
55pub struct ButtonStyle {
56 pub style: Style,
58 pub focus: Option<Style>,
60 pub armed: Option<Style>,
62 pub hover: Option<Style>,
64 pub block: Option<Block<'static>>,
66 pub armed_delay: Option<Duration>,
70
71 pub non_exhaustive: NonExhaustive,
72}
73
74#[derive(Debug)]
76pub struct ButtonState {
77 pub area: Rect,
80 pub inner: Rect,
83
84 pub hover_enabled: bool,
86 pub armed: bool,
89 pub armed_delay: Option<Duration>,
95
96 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 #[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 #[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 #[inline]
159 pub fn style(mut self, style: impl Into<Style>) -> Self {
160 self.style = style.into();
161 self
162 }
163
164 #[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 #[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 pub fn armed_delay(mut self, delay: Duration) -> Self {
182 self.armed_delay = Some(delay);
183 self
184 }
185
186 pub fn hover_style(mut self, style: impl Into<Style>) -> Self {
188 self.hover_style = Some(style.into());
189 self
190 }
191
192 #[inline]
194 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
195 self.text = text.into().centered();
196 self
197 }
198
199 pub fn left_aligned(mut self) -> Self {
201 self.text = self.text.left_aligned();
202 self
203 }
204
205 pub fn right_aligned(mut self) -> Self {
207 self.text = self.text.right_aligned();
208 self
209 }
210
211 #[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 pub fn width(&self) -> u16 {
221 self.text.width() as u16 + block_size(&self.block).width
222 }
223
224 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 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
370 pub enum ButtonOutcome {
371 Continue,
373 Unchanged,
375 Changed,
377 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 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 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
477impl 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 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 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
532pub 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
544pub fn handle_mouse_events(
546 state: &mut ButtonState,
547 event: &crossterm::event::Event,
548) -> ButtonOutcome {
549 HandleEvent::handle(state, event, MouseOnly)
550}