1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use crate::core::geometry::Rect;
use crate::core::event::{Event, EventType, KB_ESC_ESC, KB_ENTER};
use crate::core::command::{CommandId, CM_CANCEL};
use crate::terminal::Terminal;
use super::view::View;
use super::window::Window;
use std::time::Duration;
pub struct Dialog {
window: Window,
result: CommandId,
}
impl Dialog {
pub fn new(bounds: Rect, title: &str) -> Self {
Self {
window: Window::new(bounds, title),
result: CM_CANCEL,
}
}
/// Create a new modal dialog for use with Application::exec_view()
/// Matches Borland pattern: Dialog is created with SF_MODAL set, then passed to execView()
pub fn new_modal(bounds: Rect, title: &str) -> Box<Self> {
use crate::core::state::SF_MODAL;
let mut dialog = Self::new(bounds, title);
let current_state = dialog.state();
dialog.set_state(current_state | SF_MODAL);
Box::new(dialog)
}
pub fn add(&mut self, view: Box<dyn View>) {
self.window.add(view);
}
pub fn set_initial_focus(&mut self) {
self.window.set_initial_focus();
}
/// Set focus to a specific child by index
/// Matches Borland: owner->setCurrent(this, normalSelect)
pub fn set_focus_to_child(&mut self, index: usize) {
self.window.set_focus_to_child(index);
}
/// Get the number of child views
pub fn child_count(&self) -> usize {
self.window.child_count()
}
/// Get a reference to a child view by index
pub fn child_at(&self, index: usize) -> &dyn View {
self.window.child_at(index)
}
/// Get a mutable reference to a child view by index
pub fn child_at_mut(&mut self, index: usize) -> &mut dyn View {
self.window.child_at_mut(index)
}
/// Execute the dialog with its own event loop (self-contained pattern)
///
/// **Two execution patterns supported:**
///
/// **Pattern 1: Self-contained (simpler, for direct use):**
/// ```ignore
/// let mut dialog = Dialog::new(bounds, "Title");
/// dialog.add(Button::new(...));
/// let result = dialog.execute(&mut app); // Runs own event loop
/// ```
///
/// **Pattern 2: Centralized (Borland-style, via Application::exec_view):**
/// ```ignore
/// let mut dialog = Dialog::new_modal(bounds, "Title");
/// dialog.add(Button::new(...));
/// let result = app.exec_view(dialog); // App runs the modal loop
/// ```
///
/// Both patterns work identically. Pattern 1 is simpler for standalone use.
/// Pattern 2 matches Borland's TProgram::execView() architecture.
pub fn execute(&mut self, app: &mut crate::app::Application) -> CommandId {
use crate::core::state::SF_MODAL;
self.result = CM_CANCEL;
// Set modal flag - dialogs are modal by default
// Matches Borland: TDialog in modal state (tdialog.cc)
let old_state = self.state();
self.set_state(old_state | SF_MODAL);
// Event loop matching Borland's TGroup::execute() (tgroup.cc:182-195)
// IMPORTANT: We can't just delegate to window.execute() because that would
// call Group::handle_event(), but we need Dialog::handle_event() to be called
// (to handle commands and call end_modal).
//
// In Borland, TDialog inherits from TGroup, so TGroup::execute() calls
// TDialog::handleEvent() via virtual function dispatch.
//
// In Rust with composition, we must implement the execute loop here
// and call self.handle_event() to get proper polymorphic behavior.
loop {
// Draw desktop first (clears the background), then draw this dialog on top
// This is the key: dialogs that aren't on the desktop need to draw themselves
app.desktop.draw(&mut app.terminal);
self.draw(&mut app.terminal);
self.update_cursor(&mut app.terminal);
let _ = app.terminal.flush();
// Poll for event
if let Some(mut event) = app.terminal.poll_event(Duration::from_millis(50)).ok().flatten() {
// Handle the event - this calls Dialog::handle_event()
// which will call end_modal if needed
self.handle_event(&mut event);
}
// Check if dialog should close
// Dialog::handle_event() calls window.end_modal() which sets the Group's end_state
let end_state = self.window.get_end_state();
if end_state != 0 {
self.result = end_state;
break;
}
}
// Restore previous state (clear modal flag)
self.set_state(old_state);
self.result
}
}
impl View for Dialog {
fn bounds(&self) -> Rect {
self.window.bounds()
}
fn set_bounds(&mut self, bounds: Rect) {
self.window.set_bounds(bounds);
}
fn draw(&mut self, terminal: &mut Terminal) {
self.window.draw(terminal);
}
fn update_cursor(&self, terminal: &mut Terminal) {
self.window.update_cursor(terminal);
}
fn handle_event(&mut self, event: &mut Event) {
use crate::core::state::SF_MODAL;
// First let the window (and its children) handle the event
// This is critical: if a focused Memo/Editor handles Enter, it will clear the event
// Borland's TDialog calls TWindow::handleEvent() FIRST (tdialog.cc line 47)
self.window.handle_event(event);
// Now check if the event is still active after children processed it
// If a child (like Memo/Editor) handled Enter, event.what will be EventType::None
// This matches Borland's TDialog architecture (tdialog.cc lines 48-86)
match event.what {
EventType::Keyboard => {
match event.key_code {
KB_ESC_ESC => {
// Double ESC generates cancel command (lines 53-58)
*event = Event::command(CM_CANCEL);
}
KB_ENTER => {
// Enter key activates default button (lines 60-66)
// Borland converts to evBroadcast + cmDefault and re-queues
// We simplify by directly activating the default button
if let Some(cmd) = self.find_default_button_command() {
*event = Event::command(cmd);
} else {
event.clear();
}
}
_ => {}
}
}
EventType::Command => {
// Check for commands that should end modal dialogs
// Matches Borland: TDialog::handleEvent() (tdialog.cc lines 70-84)
// In Borland, ANY command that reaches the dialog (not handled by children)
// will end the modal loop. This allows custom command IDs from buttons.
if (self.state() & SF_MODAL) != 0 {
// End the modal loop with the command ID as the result
// Borland: endModal(event.message.command); clearEvent(event);
self.window.end_modal(event.command);
event.clear();
}
}
_ => {}
}
}
fn state(&self) -> crate::core::state::StateFlags {
self.window.state()
}
fn set_state(&mut self, state: crate::core::state::StateFlags) {
self.window.set_state(state);
}
fn get_end_state(&self) -> CommandId {
self.window.get_end_state()
}
fn set_end_state(&mut self, command: CommandId) {
self.window.set_end_state(command);
}
}
impl Dialog {
/// Find the default button and return its command if it's enabled
/// Returns None if no default button found or if it's disabled
/// Matches Borland's TButton::handleEvent() cmDefault broadcast handling (tbutton.cc lines 238-244)
fn find_default_button_command(&self) -> Option<CommandId> {
for i in 0..self.child_count() {
let child = self.child_at(i);
if child.is_default_button() {
// Check if the button can receive focus (i.e., not disabled)
// Borland checks: amDefault && !(state & sfDisabled)
if child.can_focus() {
return child.button_command();
} else {
// Default button is disabled
return None;
}
}
}
None
}
}