Turbo Vision Palette System¶
Overview¶
The Turbo Vision palette system provides indirect color mapping that allows views to define logical color indices that are remapped through a hierarchy of palettes until reaching actual terminal color attributes. This design enables consistent theming and color inheritance throughout the UI hierarchy.
Borland's Original Implementation¶
Concept¶
In Borland Turbo Vision (C++), each TView has:
- An owner pointer to its parent TGroup
- A getPalette() method that returns a palette for that view type
- A mapColor(uchar index) method that walks up the owner chain
Color Mapping Process¶
When a view needs to draw with a color, it calls mapColor(logicalIndex):
- View's Palette: Remap logical index through the view's own palette
- Owner Chain Walk: Walk up through
owner->owner->owner... - Parent Palettes: At each level, remap through that parent's palette
- Application Root: Reach the application, which has the final color attributes
Example in Borland C++¶
// Button wants to draw with color 3 (normal text)
Attr color = mapColor(3);
// Walk up the chain:
// 1. Button palette: 3 -> 14 (button's "normal text" maps to dialog color 14)
// 2. Dialog palette: 14 -> 45 (dialog color 14 maps to app color 45)
// 3. Application palette: 45 -> 0x2F (app color 45 is actual attribute: bright white on green)
Borland's Owner Chain¶
Each view stores a raw owner pointer to its parent, forming a linked list that mapColor() traverses.
Rust Implementation¶
QCell-Based Safe Palette Chain (v1.1.0+)¶
The Rust implementation faithfully reproduces Borland's owner-chain walk using
qcell::QCell for safe shared access, with zero unsafe code. Each view
stores an Option<PaletteChainNode> -- a reference-counted, QCell-protected
node that holds a palette and a link to its parent's node.
A single QCellOwner lives in a static OnceLock (accessed via
palette_token()), so draw() and map_color() keep their original
signatures with no token parameter threading.
For the full design rationale, thread-safety analysis, and architecture diagram, see PALETTE-SYSTEM-DESIGN.md.
Chain Setup During Draw¶
During draw(), each parent builds its palette chain node and sets it on
children before drawing them:
// Group::draw() -- Group is typically transparent (no palette)
let my_node = PaletteChainNode::new(
self.get_palette(), // None for Group
self.palette_chain.clone(), // link to parent's node (e.g. Window)
);
for child in &mut self.children {
child.set_palette_chain(Some(my_node.clone()));
child.draw(terminal);
}
Window::draw() does the same but provides a real palette (CP_BLUE_WINDOW, CP_GRAY_DIALOG, etc.) as the chain root.
Implementation in View::map_color()¶
fn map_color(&self, color_index: u8) -> Attr {
let mut color = color_index;
// Step 1: Remap through this view's own palette
if let Some(palette) = self.get_palette() {
color = palette.get(color as usize);
}
// Step 2: Walk up the QCell chain (safe, no unsafe)
if let Some(chain_node) = self.get_palette_chain() {
color = chain_node.remap_color(color);
}
// Step 3: Resolve through application palette
let app_palette = palettes::get_app_palette();
Attr::from_u8(app_palette[(color - 1) as usize])
}
Per-View Storage¶
Each view stores a single optional chain node:
struct Button {
// ... other fields
palette_chain: Option<PaletteChainNode>, // set by parent during draw
}
Benefits:
- No raw pointers or unsafe code anywhere in the view system
- Faithful reproduction of Borland's chain-walk semantics
- Original draw() and map_color() signatures preserved (no parameter changes)
- Thread-safe: static QCellOwner is Sync, nodes use Rc (!Send)
- Negligible runtime cost: one Rc clone per child per frame
Palette Definitions¶
Application Palette (CP_APP_COLOR)¶
The root palette containing actual terminal color attributes (foreground/background pairs). Matches Borland's cpColor exactly:
pub const CP_APP_COLOR: &[u8] = &[
0x71, 0x70, 0x78, 0x74, 0x20, 0x28, 0x24, 0x17, // 1-8: Desktop colors
0x1F, 0x1A, 0x31, 0x31, 0x1E, 0x71, 0x00, // 9-15: Menu colors
0x30, 0x3F, 0x3A, 0x13, 0x13, 0x3E, 0x21, 0x00, // 16-23: Cyan Window
0x70, 0x7F, 0x7A, 0x13, 0x13, 0x70, 0x7F, 0x00, // 24-31: Gray Window
0x70, 0x7F, 0x7A, 0x13, 0x13, 0x70, 0x70, 0x7F, // 32-39: Dialog
0x7E, 0x20, 0x2B, 0x2F, 0x78, 0x2E, 0x70, 0x30, // 40-47: Dialog controls
0x3F, 0x3E, 0x1F, 0x2F, 0x1A, 0x20, 0x72, 0x31, // 48-55: Dialog
0x31, 0x30, 0x2F, 0x3E, 0x31, 0x13, 0x38, 0x00, // 56-63: Dialog
// 64-74: Syntax highlighting (Blue background)
0x17, 0x1E, 0x1C, 0x1F, 0x1D, 0x1F, 0x17, 0x1A, 0x1F, 0x1B, 0x1F,
// 75-85: Syntax highlighting (Cyan background)
0x37, 0x3E, 0x3C, 0x3F, 0x3D, 0x3F, 0x37, 0x3A, 0x3F, 0x3B, 0x3F,
// 86-96: Syntax highlighting (Gray background)
0x70, 0x7E, 0x7C, 0x78, 0x7D, 0x70, 0x70, 0x72, 0x78, 0x71, 0x74,
];
Palette layout: - 1: TBackground - 2-7: TMenuView and TStatusLine - 8-15: TWindow (Blue) - 16-23: TWindow (Cyan) - 24-31: TWindow (Gray) - 32-63: TDialog - 64-74: Syntax highlighting (Blue background) - 75-85: Syntax highlighting (Cyan background) - 86-96: Syntax highlighting (Gray background)
Color attributes use format: 0xBF where:
- B = background color (high nibble)
- F = foreground color (low nibble)
Example: 0x2F = bright white (F) on green (2)
Gray Dialog Palette (CP_GRAY_DIALOG)¶
Maps dialog-level color indices to application palette indices:
pub const CP_GRAY_DIALOG: &[u8] = &[
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, // 1-10: Dialog colors map to app 32-41
42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // 11-20: More mappings
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // 21-30
62, 63, // 31-32
];
This palette provides the "gray dialog" theme where dialogs have gray backgrounds.
View-Specific Palettes¶
Each view type defines its own palette mapping its logical colors to parent (dialog) colors:
Button Palette (CP_BUTTON) - Matches Borland cpButton "\x0A\x0B\x0C\x0D\x0E\x0E\x0E\x0F":
Button color indices (when inside a Dialog): - 1: Normal → Dialog[10]=41 → App[41]=0x20 (Black on Green) - 2: Default → Dialog[11]=42 → App[42]=0x2B (LightGreen on Green) - 3: Focused → Dialog[12]=43 → App[43]=0x2F (White on Green) - 4: Disabled → Dialog[13]=44 → App[44]=0x78 (DarkGray on LightGray) - 5-7: Shortcut → Dialog[14]=45 → App[45]=0x2E (Yellow on Green) - 8: Shadow → Dialog[15]=46 → App[46]=0x70 (Black on LightGray)
Label Palette (CP_LABEL) - Matches Borland cpLabel "\x07\x08\x09\x09\x0D\x0D":
pub const CP_LABEL: &[u8] = &[
7, 8, 9, 9, 13, 13, // 6 entries for normal fg/bg, light fg/bg, disabled fg/bg
];
Label colors (when inside a Dialog): - 1: Normal fg → Dialog[7]=38 → App[38]=0x70 (Black on LightGray) - 2: Normal bg → Dialog[8]=39 → App[39]=0x7F (White on LightGray) - 3-4: Light → Dialog[9]=40 → App[40]=0x7E (Yellow on LightGray) - 5-6: Disabled → Dialog[13]=44 → App[44]=0x78 (DarkGray on LightGray)
StaticText Palette (CP_STATIC_TEXT) - Matches Borland cpStaticText "\x06":
StaticText color (when inside a Dialog): - 1: Normal → Dialog[6]=37 → App[37]=0x70 (Black on LightGray)
MenuBar Palette (CP_MENU_BAR) - Top-level view (top-level, no parent palette):
pub const CP_MENU_BAR: &[u8] = &[
2, 5, 3, 4, // Direct app palette indices (no dialog remapping)
];
MenuBar colors (NO dialog remapping, goes directly to app): - 1: Normal → App[2]=0x70 (Black on LightGray) - 2: Selected → App[5]=0x20 (Black on Green) - 3: Disabled → App[3]=0x78 (DarkGray on LightGray) - 4: Shortcut → App[4]=0x74 (Red on LightGray)
Complete Color Mapping Example¶
Let's trace how a Button's focused text (logical color 3) becomes a terminal color when in a Dialog:
Step 1: Button's Palette¶
Button's "focused text" maps to dialog color 12.Step 2: Check Owner Type¶
Step 3: Gray Dialog Palette¶
Dialog color 12 maps to application color 43.Step 4: Application Palette¶
Application color 43 is the actual terminal attribute:0x2F = White on Green.
Final Result¶
Example: MenuBar (Top-Level View)¶
Let's trace how a MenuBar's selected item (logical color 2) becomes a terminal color:
Step 1: MenuBar's Palette¶
MenuBar's "selected" maps to app color 5.Step 2: Check Owner Type¶
Step 3: Application Palette (Direct)¶
Application color 5 is the actual terminal attribute:0x20 = Black on Green.
Final Result¶
Comparison: Borland vs Rust¶
| Aspect | Borland C++ | Rust Implementation |
|---|---|---|
| Owner Storage | Raw TView* owner pointer |
PaletteChainNode (Rc\<QCell>) |
| Chain Traversal | Runtime walk via owner->owner |
QCell chain walk via remap_color() |
| Safety | Unsafe raw pointers | 100% safe Rust (zero unsafe in views) |
| Flexibility | Dynamic, any hierarchy depth | Dynamic, any hierarchy depth (faithful reproduction) |
| Performance | Pointer dereferences + virtual calls | Direct palette lookups + enum check |
| Visual Output | Depends on actual hierarchy | Same colors via context-aware remapping |
| Context Awareness | Implicit (via owner chain) | Implicit (via QCell palette chain) |
Advantages of the Rust Approach¶
Safety¶
- ✅ No undefined behavior from invalid pointers
- ✅ No crashes from moved views
- ✅ Compiler-verified correctness
Simplicity¶
- ✅ Easier to understand (no pointer chasing)
- ✅ Easier to debug (deterministic mapping)
- ✅ Less code complexity
Performance¶
- ✅ No pointer dereferencing overhead
- ✅ No virtual function calls up the chain
- ✅ Direct array lookups
Limitations¶
Dynamic Palette Chain¶
The QCell-based palette chain supports arbitrary nesting depth, faithfully
reproducing Borland's dynamic owner chain traversal. Any view hierarchy
(Window, Dialog, nested Groups, custom containers) works automatically
because the chain is built from each view's actual get_palette() at draw time.
- Runtime-switchable palette chains
When This Matters¶
The context limitation only affects advanced scenarios like: - Custom container types with unique palettes (rare) - Deeply nested groups with different themes (uncommon) - Dynamic palette switching at runtime (unusual)
For standard Turbo Vision applications (Desktop → Window/Dialog → Controls), the context-aware remapping produces identical visual results to Borland's dynamic owner chain traversal.
Testing and Validation¶
The palette system includes comprehensive regression tests:
- 9 palette regression tests in tests/palette_regression_tests.rs
- Tests verify Borland-accurate colors for all UI components
- Tests cover both Dialog-context and top-level views
- All tests ensure color stability across changes
Runtime Palette Customization¶
The palette system supports runtime customization of the entire application palette, allowing you to create custom themes:
Using Application::set_palette()¶
The Application::set_palette() method provides a convenient way to change the application palette with automatic redrawing:
use turbo_vision::app::Application;
let mut app = Application::new()?;
// Create a custom dark theme palette (63 bytes)
// Each byte encodes: (foreground << 4) | background
let dark_palette = vec![
0x08, 0x0F, 0x08, 0x0E, 0x0B, 0x0A, 0x0C, 0x01, // Desktop
0xF1, 0xE1, 0xF3, 0xF3, 0xF1, 0x08, 0x00, // Menu
// ... 63 bytes total
];
// Set the custom palette (redraw happens automatically!)
app.set_palette(Some(dark_palette));
// Reset to default Borland palette
app.set_palette(None);
How It Works¶
- Automatic Redraw:
set_palette()automatically callsneeds_redraw()when the palette changes - Change Detection: Only triggers redraw if the palette actually differs from the current one
- Thread-Local Storage: Custom palette is stored in a thread-local
RefCell<Option<Vec<u8>>> - Transparent Remapping: All views automatically use the new palette through
map_color()
Custom Palette Format¶
The application palette (CP_APP_COLOR) is a 63-byte array where each byte encodes a color attribute:
Byte format: 0xBF
B = Background color (high nibble, 0-F)
F = Foreground color (low nibble, 0-F)
Color values:
0=Black, 1=Blue, 2=Green, 3=Cyan, 4=Red, 5=Magenta, 6=Brown, 7=LightGray
8=DarkGray, 9=LightBlue, A=LightGreen, B=LightCyan, C=LightRed,
D=LightMagenta, E=Yellow, F=White
Palette Layout (indices 1-63)¶
1-8: Desktop colors
9-15: Menu and StatusLine
16-23: Cyan Window theme
24-31: Gray Window theme
32-63: Dialog and control colors
Example: Creating Themes¶
See examples/palette_themes_demo.rs for a complete example with multiple themes:
// Dark theme with dark backgrounds
let dark_palette = vec![
0x08, 0x0F, 0x08, 0x0E, 0x0B, 0x0A, 0x0C, 0x01,
0xF1, 0xE1, 0xF3, 0xF3, 0xF1, 0x08, 0x00,
// ... rest of palette
];
// High-contrast theme (black on white, white on black)
let contrast_palette = vec![
0x0F, 0xF0, 0x0F, 0xE0, 0xF0, 0xE0, 0xF0, 0xF0,
0x0F, 0xE0, 0x0F, 0x0F, 0x0F, 0x0F, 0x00,
// ... rest of palette
];
// Switch between themes
match theme_choice {
ThemeChoice::Dark => app.set_palette(Some(dark_palette)),
ThemeChoice::Contrast => app.set_palette(Some(contrast_palette)),
ThemeChoice::Default => app.set_palette(None),
}
Implementation Details¶
The set_palette() method in Application:
pub fn set_palette(&mut self, palette: Option<Vec<u8>>) {
use crate::core::palette::palettes;
// Get current palette to check if it's actually changing
let current_palette = palettes::get_app_palette();
let is_changing = match &palette {
Some(new_palette) => new_palette != ¤t_palette,
None => current_palette != palettes::CP_APP_COLOR,
};
// Set the new palette
palettes::set_custom_palette(palette);
// Trigger redraw only if the palette actually changed
if is_changing {
self.needs_redraw = true;
}
}
Low-Level API¶
For advanced use cases, you can use the low-level palette API:
use turbo_vision::core::palette::palettes;
// Set palette manually (no automatic redraw)
palettes::set_custom_palette(Some(custom_palette));
// Get current palette (custom or default)
let current = palettes::get_app_palette();
// Manually trigger redraw
app.needs_redraw();
Testing and Validation¶
The palette system includes comprehensive regression tests:
- 9 palette regression tests in tests/palette_regression_tests.rs
- Tests verify Borland-accurate colors for all UI components
- Tests cover both Dialog-context and top-level views
- All tests ensure color stability across changes
Future Enhancements¶
If dynamic palette chains are needed, safe alternatives include:
Option 1: Palette Caching¶
When a view is added to a parent, compute and cache the full palette chain:
struct View {
// Cache the resolved palette chain when added to parent
cached_palette_chain: Option<Palette>,
}
Option 2: Rc>¶
Use reference-counted smart pointers instead of raw pointers:
Option 3: Callback-Based Resolution¶
Pass a color resolver function during drawing:
Conclusion¶
The current palette system eliminates unsafe code while maintaining visual compatibility with Borland Turbo Vision. By using QCell-based palette chain nodes instead of raw owner pointers, we achieve:
- 100% memory safety (simple enum field, no raw pointers, no unsafe code)
- Identical visual output for standard UI layouts (verified by regression tests)
- Simpler implementation with better performance (direct lookups, no pointer chasing)
- Context-aware remapping that matches Borland's behavior
- Maintained compatibility with the Borland design philosophy
The context-aware palette system is a pragmatic design that prioritizes safety and simplicity while providing the flexibility needed for real-world Turbo Vision applications. The three context types (None, Window, Dialog) cover all standard use cases, and the comprehensive test suite ensures ongoing correctness.