Skip to main content

lv_tui/widgets/
dialog.rs

1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::node::Node;
6use crate::render::RenderCx;
7use crate::style::{Border, Color, Style};
8
9/// A dialog wrapper that adds Esc/Enter key bindings and a footer hint.
10///
11/// Renders the child inside a [`Border::Rounded`] block and shows available
12/// keyboard actions in a dimmed footer line. Check [`Dialog::confirmed`] or
13/// [`Dialog::cancelled`] after interaction to respond to user choices.
14pub struct Dialog {
15    inner: Node,
16    cancelled: bool,
17    confirmed: bool,
18    rect: Rect,
19    style: Style,
20    border: Border,
21}
22
23impl Dialog {
24    /// Creates a dialog wrapping the given child component.
25    pub fn new(child: impl Component + 'static) -> Self {
26        Self {
27            inner: Node::new(child),
28            cancelled: false,
29            confirmed: false,
30            rect: Rect::default(),
31            style: Style::default(),
32            border: Border::Rounded,
33        }
34    }
35
36    /// Builder: sets the border style.
37    pub fn border(mut self, border: Border) -> Self {
38        self.border = border;
39        self
40    }
41
42    /// Builder: sets the dialog style.
43    pub fn style(mut self, style: Style) -> Self {
44        self.style = style;
45        self
46    }
47
48    /// Returns true if the dialog was cancelled (Esc pressed).
49    pub fn cancelled(&self) -> bool { self.cancelled }
50
51    /// Returns true if the dialog was confirmed (Enter pressed).
52    pub fn confirmed(&self) -> bool { self.confirmed }
53
54    /// Resets the cancelled/confirmed state.
55    pub fn reset(&mut self, cx: &mut EventCx) {
56        self.cancelled = false;
57        self.confirmed = false;
58        cx.invalidate_paint();
59    }
60}
61
62impl Component for Dialog {
63    fn render(&self, cx: &mut RenderCx) {
64        // Use our own stored rect
65        let r = self.rect;
66
67        // Draw border
68        cx.buffer.draw_border(r, self.border, &cx.style);
69
70        // Render inner content with padding
71        let inner_rect = r.inner(crate::geom::Insets::all(2));
72        let saved = self.inner.rect();
73        self.inner.set_rect(inner_rect);
74        self.inner.render(cx.buffer, cx.focused_id);
75        self.inner.set_rect(saved);
76
77        // Footer hint
78        let footer_y = r.y.saturating_add(r.height.saturating_sub(1));
79        let footer_style = Style::default().fg(Color::Gray);
80        cx.buffer.write_text(
81            crate::geom::Pos { x: r.x.saturating_add(2), y: footer_y },
82            r,
83            "Enter: confirm  Esc: cancel",
84            &footer_style,
85        );
86        // Restore cursor position — Dialog doesn't advance the cursor
87        cx.cursor.x = r.x;
88        cx.cursor.y = r.y;
89    }
90
91    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
92        let child_size = self.inner.measure(constraint);
93        Size {
94            width: child_size.width.saturating_add(4),   // border + padding
95            height: child_size.height.saturating_add(4), // border + padding + footer
96        }
97    }
98
99    fn event(&mut self, event: &Event, cx: &mut EventCx) {
100        match event {
101            Event::Focus | Event::Blur | Event::Tick => return,
102            _ => {}
103        }
104
105        // Only handle confirm/cancel during Target or Bubble phase
106        if cx.phase() != crate::event::EventPhase::Capture {
107            if let Event::Key(key_event) = event {
108                match &key_event.key {
109                    crate::event::Key::Esc => {
110                        self.cancelled = true;
111                        cx.invalidate_paint();
112                        return;
113                    }
114                    crate::event::Key::Enter => {
115                        self.confirmed = true;
116                        cx.invalidate_paint();
117                        return;
118                    }
119                    _ => {}
120                }
121            }
122        }
123
124        // Forward other events to child in Capture phase only
125        if cx.phase() == crate::event::EventPhase::Capture {
126            let mut child_cx = EventCx::with_task_sender(
127                &mut self.inner.dirty, cx.global_dirty, cx.quit,
128                cx.phase, cx.propagation_stopped, cx.task_sender.clone(),
129            );
130            self.inner.component.event(event, &mut child_cx);
131        }
132    }
133
134    fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
135        self.rect = rect;
136        let inner = rect.inner(crate::geom::Insets::all(2));
137        self.inner.layout(inner);
138    }
139
140    fn for_each_child(&self, f: &mut dyn FnMut(&Node)) { f(&self.inner); }
141    fn for_each_child_mut(&mut self, f: &mut dyn FnMut(&mut Node)) { f(&mut self.inner); }
142    fn focusable(&self) -> bool { true }
143    fn style(&self) -> Style { self.style.clone() }
144}