turbo_vision/core/status_data.rs
1// (C) 2025 - Enzo Lombardi
2
3//! Status line data structures - declarative status bar building with command-based visibility.
4// Borland-compatible status line data structures
5// Matches the original Turbo Vision TStatusItem, TStatusDef architecture
6//
7// This module provides data structures for building status lines in a declarative way,
8// matching Borland's approach while being Rust-idiomatic.
9
10use crate::core::command::CommandId;
11use crate::core::event::KeyCode;
12
13/// Status line item - displays text and responds to keyboard shortcuts
14///
15/// Matches Borland: `TStatusItem`
16///
17/// In Borland, `TStatusItem` is a linked list node with a `next` pointer.
18/// In Rust, we use Vec for type safety, but provide builder methods
19/// that match Borland's ergonomics.
20#[derive(Clone, Debug)]
21pub struct StatusItem {
22 /// Display text (use ~x~ to mark accelerator key)
23 pub text: String,
24 /// Keyboard shortcut
25 pub key_code: KeyCode,
26 /// Command to execute when clicked or shortcut pressed
27 pub command: CommandId,
28}
29
30impl StatusItem {
31 pub fn get_accelerator(&self) -> Option<char> {
32 let mut chars = self.text.chars();
33 while let Some(ch) = chars.next() {
34 if ch == '~' {
35 // Next char is the accelerator
36 if let Some(accel) = chars.next() {
37 return Some(accel.to_ascii_lowercase());
38 }
39 }
40 }
41 None
42 }
43}
44
45/// Builder for creating status items with a fluent API.
46///
47/// # Examples
48///
49/// ```ignore
50/// use turbo_vision::core::status_data::StatusItemBuilder;
51///
52/// let item = StatusItemBuilder::new()
53/// .text("~F1~ Help")
54/// .key_code(0x3B00)
55/// .command(100)
56/// .build();
57/// ```
58pub struct StatusItemBuilder {
59 text: Option<String>,
60 key_code: KeyCode,
61 command: CommandId,
62}
63
64impl StatusItemBuilder {
65 /// Creates a new `StatusItemBuilder` with default values.
66 pub fn new() -> Self {
67 Self {
68 text: None,
69 key_code: 0,
70 command: 0,
71 }
72 }
73
74 /// Sets the status item text (required).
75 /// Use ~x~ to mark the accelerator key.
76 #[must_use]
77 pub fn text(mut self, text: impl Into<String>) -> Self {
78 self.text = Some(text.into());
79 self
80 }
81
82 /// Sets the keyboard shortcut key code.
83 #[must_use]
84 pub fn key_code(mut self, key_code: KeyCode) -> Self {
85 self.key_code = key_code;
86 self
87 }
88
89 /// Binds the item to a key chord such as `"Alt+X"` or `"F10"`.
90 ///
91 /// # Panics
92 ///
93 /// Panics on a chord `parse_key_chord` does not understand.
94 #[must_use]
95 pub fn key(mut self, chord: &str) -> Self {
96 self.key_code = crate::core::menu_data::key_code_for_chord(chord);
97 self
98 }
99
100 /// Sets the command to execute.
101 #[must_use]
102 pub fn command(mut self, command: CommandId) -> Self {
103 self.command = command;
104 self
105 }
106
107 /// Builds the `StatusItem`.
108 ///
109 /// # Panics
110 ///
111 /// Panics if required fields (text) are not set.
112 pub fn build(self) -> StatusItem {
113 let text = self.text.expect("StatusItem text must be set");
114 StatusItem {
115 text,
116 key_code: self.key_code,
117 command: self.command,
118 }
119 }
120}
121
122impl Default for StatusItemBuilder {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128/// Status line definition - defines which items are visible for a command set range
129///
130/// Matches Borland: `TStatusDef`
131///
132/// In Borland, `TStatusDef` is a linked list node with:
133/// - `min`, `max`: command range for which this def is active
134/// - `items`: pointer to first item in linked list
135/// - `next`: pointer to next status def
136///
137/// In Rust, we use Vec for type safety and provide convenient builders.
138#[derive(Clone, Debug)]
139pub struct StatusDef {
140 /// Minimum command ID for which this definition is active
141 pub min: u16,
142 /// Maximum command ID for which this definition is active
143 pub max: u16,
144 /// Status items to display
145 pub items: Vec<StatusItem>,
146}
147
148impl StatusDef {
149 /// Create a new status definition
150 ///
151 /// Matches Borland: `TStatusDef(min, max, items)`
152 ///
153 /// # Example
154 /// ```ignore
155 /// let def = StatusDef::new(0, 0xFFFF, vec![
156 /// StatusItemBuilder::new().text("~F1~ Help").key("F1").command(CM_HELP).build(),
157 /// StatusItemBuilder::new().text("~Alt+X~ Exit").key("Alt+X").command(CM_QUIT).build(),
158 /// ]);
159 /// ```
160 pub fn new(min: u16, max: u16, items: Vec<StatusItem>) -> Self {
161 Self { min, max, items }
162 }
163
164 /// Create a status definition for all command ranges (default)
165 ///
166 /// Matches Borland: `TStatusDef(0, 0xFFFF, items)`
167 pub fn default_range(items: Vec<StatusItem>) -> Self {
168 Self {
169 min: 0,
170 max: 0xFFFF,
171 items,
172 }
173 }
174
175 /// Check if this definition applies to the given command set
176 ///
177 /// In Borland, the status line checks which definition's range
178 /// matches the currently focused view's command set.
179 pub fn applies_to(&self, command_set: u16) -> bool {
180 command_set >= self.min && command_set <= self.max
181 }
182
183 /// Add an item to this definition
184 pub fn add(&mut self, item: StatusItem) {
185 self.items.push(item);
186 }
187
188 /// Get the number of items
189 pub fn len(&self) -> usize {
190 self.items.len()
191 }
192
193 /// Check if definition has no items
194 pub fn is_empty(&self) -> bool {
195 self.items.is_empty()
196 }
197}
198
199/// Builder for creating status definitions with a fluent API.
200///
201/// # Examples
202///
203/// ```ignore
204/// use turbo_vision::core::status_data::{StatusDefBuilder, StatusItem};
205///
206/// let def = StatusDefBuilder::new()
207/// .range(0, 100)
208/// .add_item(StatusItemBuilder::new().text("~F1~ Help").key_code(0x3B00).command(100).build())
209/// .add_item(StatusItemBuilder::new().text("~F2~ Save").key_code(0x3C00).command(101).build())
210/// .build();
211/// ```
212pub struct StatusDefBuilder {
213 min: u16,
214 max: u16,
215 items: Vec<StatusItem>,
216}
217
218impl StatusDefBuilder {
219 /// Creates a new `StatusDefBuilder` with default values (full range: 0-0xFFFF).
220 pub fn new() -> Self {
221 Self {
222 min: 0,
223 max: 0xFFFF,
224 items: Vec::new(),
225 }
226 }
227
228 /// Sets the command range (default: 0-0xFFFF).
229 #[must_use]
230 pub fn range(mut self, min: u16, max: u16) -> Self {
231 self.min = min;
232 self.max = max;
233 self
234 }
235
236 /// Sets the minimum command ID.
237 #[must_use]
238 pub fn min(mut self, min: u16) -> Self {
239 self.min = min;
240 self
241 }
242
243 /// Sets the maximum command ID.
244 #[must_use]
245 pub fn max(mut self, max: u16) -> Self {
246 self.max = max;
247 self
248 }
249
250 /// Adds a status item to the definition.
251 #[must_use]
252 pub fn add_item(mut self, item: StatusItem) -> Self {
253 self.items.push(item);
254 self
255 }
256
257 /// Sets all items at once.
258 #[must_use]
259 pub fn items(mut self, items: Vec<StatusItem>) -> Self {
260 self.items = items;
261 self
262 }
263
264 /// Builds the `StatusDef`.
265 pub fn build(self) -> StatusDef {
266 StatusDef {
267 min: self.min,
268 max: self.max,
269 items: self.items,
270 }
271 }
272}
273
274impl Default for StatusDefBuilder {
275 fn default() -> Self {
276 Self::new()
277 }
278}
279
280/// Status line configuration - collection of status definitions
281///
282/// Matches Borland: linked list of `TStatusDef`
283///
284/// In Borland, status defs are linked via `next` pointers.
285/// In Rust, we use Vec for type safety.
286#[derive(Clone, Debug)]
287pub struct StatusLine {
288 /// Status definitions (evaluated in order)
289 pub defs: Vec<StatusDef>,
290}
291
292impl StatusLine {
293 /// Create a new status line configuration
294 pub fn new(defs: Vec<StatusDef>) -> Self {
295 Self { defs }
296 }
297
298 /// Create a status line with a single default definition
299 pub fn single(items: Vec<StatusItem>) -> Self {
300 Self {
301 defs: vec![StatusDef::default_range(items)],
302 }
303 }
304
305 /// Get the status definition that applies to the given command set
306 ///
307 /// Matches Borland: `TStatusLine::update()` logic
308 pub fn get_def_for(&self, command_set: u16) -> Option<&StatusDef> {
309 // Return first matching definition (Borland behavior)
310 self.defs.iter().find(|def| def.applies_to(command_set))
311 }
312
313 /// Add a status definition
314 pub fn add_def(&mut self, def: StatusDef) {
315 self.defs.push(def);
316 }
317}
318
319/// Builder for constructing status line configurations fluently
320///
321/// This provides a Borland-style builder API while being Rust-idiomatic.
322///
323/// # Example (Borland-style)
324/// ```ignore
325/// let status = StatusLineBuilder::new()
326/// .add_def(0, 0xFFFF, vec![
327/// StatusItemBuilder::new().text("~F1~ Help").key("F1").command(CM_HELP).build(),
328/// StatusItemBuilder::new().text("~Alt+X~ Exit").key("Alt+X").command(CM_QUIT).build(),
329/// ])
330/// .build();
331/// ```
332pub struct StatusLineBuilder {
333 defs: Vec<StatusDef>,
334}
335
336impl StatusLineBuilder {
337 /// Create a new status line builder
338 pub fn new() -> Self {
339 Self { defs: Vec::new() }
340 }
341
342 /// Add a status definition with command range
343 ///
344 /// Matches Borland: `TStatusDef(min, max, items)`
345 pub fn add_def(mut self, min: u16, max: u16, items: Vec<StatusItem>) -> Self {
346 self.defs.push(StatusDef::new(min, max, items));
347 self
348 }
349
350 /// Add a default status definition (applies to all command sets)
351 pub fn add_default_def(mut self, items: Vec<StatusItem>) -> Self {
352 self.defs.push(StatusDef::default_range(items));
353 self
354 }
355
356 /// Build the status line configuration
357 pub fn build(self) -> StatusLine {
358 StatusLine::new(self.defs)
359 }
360}
361
362impl Default for StatusLineBuilder {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_status_def() {
374 let def = StatusDef::new(
375 0,
376 100,
377 vec![
378 StatusItemBuilder::new()
379 .text("~F1~ Help")
380 .key_code(0x3B00)
381 .command(100)
382 .build(),
383 StatusItemBuilder::new()
384 .text("~Alt+X~ Exit")
385 .key("Alt+X")
386 .command(101)
387 .build(),
388 ],
389 );
390
391 assert!(def.applies_to(50));
392 assert!(def.applies_to(0));
393 assert!(def.applies_to(100));
394 assert!(!def.applies_to(101));
395 assert_eq!(def.len(), 2);
396 }
397
398 #[test]
399 fn test_status_line_builder() {
400 let status = StatusLineBuilder::new()
401 .add_def(
402 0,
403 100,
404 vec![
405 StatusItemBuilder::new()
406 .text("~F1~ Help")
407 .key_code(0x3B00)
408 .command(100)
409 .build(),
410 StatusItemBuilder::new()
411 .text("~F2~ Save")
412 .key_code(0x3C00)
413 .command(101)
414 .build(),
415 ],
416 )
417 .add_def(
418 101,
419 200,
420 vec![
421 StatusItemBuilder::new()
422 .text("~F1~ Help")
423 .key_code(0x3B00)
424 .command(100)
425 .build(),
426 StatusItemBuilder::new()
427 .text("~Esc~ Cancel")
428 .key_code(0x011B)
429 .command(102)
430 .build(),
431 ],
432 )
433 .build();
434
435 assert_eq!(status.defs.len(), 2);
436
437 let def1 = status.get_def_for(50);
438 assert!(def1.is_some());
439 assert_eq!(def1.unwrap().len(), 2);
440
441 let def2 = status.get_def_for(150);
442 assert!(def2.is_some());
443 assert_eq!(def2.unwrap().len(), 2);
444 }
445
446 #[test]
447 fn test_accelerator() {
448 let item = StatusItemBuilder::new()
449 .text("~F1~ Help")
450 .key_code(0x3B00)
451 .command(100)
452 .build();
453 assert_eq!(item.get_accelerator(), Some('f'));
454 }
455}