1use 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#[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#[derive(Debug, Clone)]
54pub struct ButtonStyle {
55 pub style: Style,
57 pub block: Option<Block<'static>>,
59 pub border_style: Option<Style>,
60 pub title_style: Option<Style>,
61 pub focus: Option<Style>,
63 pub armed: Option<Style>,
65 pub hover: Option<Style>,
67 pub armed_delay: Option<Duration>,
71
72 pub non_exhaustive: NonExhaustive,
73}
74
75#[derive(Debug)]
77pub struct ButtonState {
78 pub area: Rect,
81 pub inner: Rect,
84 pub armed: bool,
87 pub armed_delay: Option<Duration>,
94
95 pub focus: FocusFlag,
98
99 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 #[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 #[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 #[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 #[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 #[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 #[inline]
194 pub fn armed_delay(mut self, delay: Duration) -> Self {
195 self.armed_delay = Some(delay);
196 self
197 }
198
199 #[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 #[inline]
208 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
209 self.text = text.into().centered();
210 self
211 }
212
213 #[inline]
215 pub fn left_aligned(mut self) -> Self {
216 self.text = self.text.left_aligned();
217 self
218 }
219
220 #[inline]
222 pub fn right_aligned(mut self) -> Self {
223 self.text = self.text.right_aligned();
224 self
225 }
226
227 #[inline]
229 pub fn block(mut self, block: Block<'a>) -> Self {
230 self.block = Some(block.style(self.style));
231 self
232 }
233
234 #[inline]
236 pub fn width(&self) -> u16 {
237 self.text.width() as u16 + block_size(&self.block).width
238 }
239
240 #[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 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
394 pub enum ButtonOutcome {
395 Continue,
397 Unchanged,
399 Changed,
401 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 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 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
501impl 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 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 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
556pub 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
568pub fn handle_mouse_events(
570 state: &mut ButtonState,
571 event: &crossterm::event::Event,
572) -> ButtonOutcome {
573 HandleEvent::handle(state, event, MouseOnly)
574}