Skip to main content

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}