GUI Architecture
This document provides a comprehensive overview of the GUI architecture for the Mosaic Art Generator, detailing the design patterns, component structure, and technical implementation.
Table of Contents
- Overview
- Architecture Pattern
- Component Structure
- State Management
- Message System
- File Operations
- Testing Architecture
- Platform Integration
Overview
The GUI application is built using the iced framework, a modern Rust GUI library that follows the Elm architecture pattern. This provides a predictable, type-safe approach to building interactive applications with clear separation of concerns.
Key Design Principles
- Type Safety: Leverages Rust's type system for compile-time correctness
- Immutability: State changes through pure functions and message passing
- Composability: UI components built from reusable elements
- Testability: Clear separation enables comprehensive unit testing
- Cross-Platform: Single codebase for Windows, macOS, and Linux
Architecture Pattern
Elm Architecture
The application follows the Elm architecture with four core components:
┌─────────────────────────────────────────────────────────────┐
│ Elm Architecture │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Model │ Message │ Update │
│ (State) │ (Events) │ (State Logic) │
├─────────────────┴─────────────────┴─────────────────────────┤
│ View │
│ (UI Rendering) │
└─────────────────────────────────────────────────────────────┘
Model (State)
pub struct MosaicApp {
// File paths
target_path: String,
material_path: String,
output_path: String,
// Application settings
settings: MosaicSettings,
// UI state
theme: Theme,
pending_selection: Option<FileSelectionType>,
// Input field states (for real-time validation)
grid_w_input: String,
grid_h_input: String,
total_tiles_input: String,
max_materials_input: String,
color_adjustment_input: String,
}
Message (Events)
#[derive(Debug, Clone)]
pub enum Message {
// File selection events
TargetPathChanged(String),
MaterialPathChanged(String),
OutputPathChanged(String),
// File dialog events
OpenTargetFile,
OpenMaterialFolder,
SaveOutputFile,
FileSelected(Option<PathBuf>),
// Settings events
GridWidthChanged(String),
GridHeightChanged(String),
TotalTilesChanged(String),
AutoCalculateToggled(bool),
MaxMaterialsChanged(String),
ColorAdjustmentChanged(String),
OptimizationToggled(bool),
// Action events
CalculateGrid,
GenerateMosaic,
ToggleTheme,
}
Update (State Logic)
The update function handles all state transitions:
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::CalculateGrid => {
if let Some(total_tiles) = self.settings.total_tiles {
let aspect_ratio = 16.0 / 9.0;
let w = ((total_tiles as f32 * aspect_ratio).sqrt()).round() as u32;
let h = (total_tiles / w).max(1);
self.settings.grid_w = w;
self.settings.grid_h = h;
self.grid_w_input = w.to_string();
self.grid_h_input = h.to_string();
}
}
// ... other message handlers
}
Command::none()
}
View (UI Rendering)
The view function creates the UI layout:
fn view(&self) -> Element<'_, Self::Message> {
let content = column![
text("Mosaic Art Generator").size(32),
files_section,
grid_section,
advanced_section,
controls
]
.padding(20);
content.into()
}
Component Structure
File Structure
src/gui/
├── main.rs # Entry point and iced settings
├── app_full.rs # Complete application implementation
├── app_working.rs # Intermediate development version
└── app_simple.rs # Minimal test version
Component Hierarchy
MosaicApp (Application)
├── Title Section
├── File Selection Section
│ ├── Target Image Selection
│ ├── Material Directory Selection
│ └── Output Path Selection
├── Grid Settings Section
│ ├── Auto-Calculate Toggle
│ ├── Total Tiles Input
│ ├── Calculate Grid Button
│ ├── Grid Width Input
│ └── Grid Height Input
├── Advanced Settings Section
│ ├── Max Materials Input
│ ├── Color Adjustment Input
│ └── Optimization Toggle
└── Controls Section
├── Generate Mosaic Button
└── Toggle Theme Button
UI Element Composition
The UI is built using iced's widget combinators:
// File selection component
let files_section = column![
text("File Selection").size(20),
column![
text("Target Image:"),
row![
text_input("Enter target image path", &self.target_path)
.on_input(Message::TargetPathChanged),
button("Browse").on_press(Message::OpenTargetFile)
]
],
// ... more file selection components
];
State Management
Settings Management
#[derive(Debug, Clone)]
pub struct MosaicSettings {
pub grid_w: u32,
pub grid_h: u32,
pub total_tiles: Option<u32>,
pub auto_calculate: bool,
pub max_materials: usize,
pub color_adjustment: f32,
pub enable_optimization: bool,
}
impl Default for MosaicSettings {
fn default() -> Self {
Self {
grid_w: 50,
grid_h: 28,
total_tiles: Some(1400),
auto_calculate: true,
max_materials: 500,
color_adjustment: 0.3,
enable_optimization: true,
}
}
}
Input Validation
Real-time validation is implemented through string inputs with parsing:
Message::GridWidthChanged(value) => {
self.grid_w_input = value.clone();
if let Ok(w) = value.parse::<u32>() {
self.settings.grid_w = w;
}
}
Message::ColorAdjustmentChanged(value) => {
self.color_adjustment_input = value.clone();
if let Ok(adj) = value.parse::<f32>() {
self.settings.color_adjustment = adj.clamp(0.0, 1.0);
}
}
Auto Grid Calculation
Intelligent grid calculation algorithm:
fn calculate_optimal_grid(total_tiles: u32, aspect_ratio: f32) -> (u32, u32) {
let w = ((total_tiles as f32 * aspect_ratio).sqrt()).round() as u32;
let h = (total_tiles / w).max(1);
(w, h)
}
Message System
Event Flow
User Input → Message → Update → State Change → View Re-render
Async Operations
File dialogs use iced's Command system for async operations:
Message::OpenTargetFile => {
self.pending_selection = Some(FileSelectionType::Target);
return Command::perform(
async {
rfd::AsyncFileDialog::new()
.add_filter("images", &["png", "jpg", "jpeg"])
.pick_file()
.await
.map(|handle| handle.path().to_path_buf())
},
Message::FileSelected,
);
}
Message Routing
The pending selection system tracks which file dialog is active:
#[derive(Debug, Clone)]
enum FileSelectionType {
Target,
Material,
Output,
}
Message::FileSelected(path) => {
if let (Some(path), Some(selection_type)) = (path, &self.pending_selection) {
match selection_type {
FileSelectionType::Target => {
self.target_path = path.to_string_lossy().to_string();
}
FileSelectionType::Material => {
self.material_path = path.to_string_lossy().to_string();
}
FileSelectionType::Output => {
self.output_path = path.to_string_lossy().to_string();
}
}
}
self.pending_selection = None;
}
File Operations
Native File Dialogs
Integration with platform-native file dialogs using the rfd
crate:
// File picker for images
rfd::AsyncFileDialog::new()
.add_filter("images", &["png", "jpg", "jpeg"])
.pick_file()
.await
// Folder picker for materials
rfd::AsyncFileDialog::new()
.pick_folder()
.await
// Save dialog for output
rfd::AsyncFileDialog::new()
.add_filter("images", &["png", "jpg", "jpeg"])
.save_file()
.await
Path Handling
Cross-platform path handling with proper string conversion:
// Convert PathBuf to display string
path.to_string_lossy().to_string()
// Platform-agnostic path representation
PathBuf::from(path_string)
Testing Architecture
Unit Test Structure
GUI tests are integrated into the main test suite:
#[cfg(test)]
pub mod gui {
pub mod app_full {
include!("gui/app_full.rs");
}
}
#[test]
fn test_gui_mosaic_settings_default() {
let settings = gui::app_full::MosaicSettings::default();
assert_eq!(settings.grid_w, 50);
assert_eq!(settings.grid_h, 28);
assert!(settings.auto_calculate);
}
Test Categories
- Settings Tests: Validate default values and constraints
- Grid Calculation Tests: Verify auto-calculation algorithms
- Input Validation Tests: Test parsing and clamping
- File Path Tests: Ensure proper path handling
- State Transition Tests: Verify message handling
Test Data Isolation
Tests use temporary paths and mock data:
#[test]
fn test_path_handling() {
use std::path::PathBuf;
let test_paths = vec![
"/home/user/image.png",
"C:\\Users\\User\\image.jpg",
"relative/path/image.jpeg",
];
for path_str in test_paths {
let path = PathBuf::from(path_str);
let lossy_string = path.to_string_lossy().to_string();
assert!(!lossy_string.is_empty());
}
}
Platform Integration
Windows Integration
#![windows_subsystem = "windows"] // Prevents terminal window
// Windows-specific settings
let settings = Settings {
window: iced::window::Settings {
size: iced::Size::new(1200.0, 800.0),
position: iced::window::Position::Centered,
decorations: true,
// ... other Windows-specific options
},
// ...
};
macOS Integration
- Native Cocoa file dialogs
- Menu bar integration
- Retina display support
Linux Integration
- GTK+ file dialogs
- Wayland and X11 support
- Font configuration integration
Theme Support
Built-in theme switching:
Message::ToggleTheme => {
self.theme = match self.theme {
Theme::Light => Theme::Dark,
Theme::Dark => Theme::Light,
_ => Theme::Light,
};
}
fn theme(&self) -> Self::Theme {
self.theme.clone()
}
Performance Considerations
Memory Usage
- Minimal GUI overhead
- Efficient string handling for input fields
- Lazy evaluation of UI elements
Responsiveness
- Async file operations prevent blocking
- Real-time input validation
- Efficient re-rendering with virtual DOM
Resource Management
// Efficient string updates
self.grid_w_input = value.clone(); // Only when needed
// Shared immutable data
Arc<Tile> // For material data sharing
Robustness and Fallback System (Added 2025-01-11)
The GUI implements a comprehensive three-stage fallback system that ensures no grid cells remain empty, providing complete feature parity with the CLI version's robustness.
Technical Implementation
The fallback system operates through three distinct stages in the find_and_use_best_tile_with_position
method:
// Stage 1: Primary selection with all constraints
if let Some(tile) = self.find_best_tile_with_constraints(
&target_lab_color,
&material_colors,
&kdtree,
&mut usage_tracker,
position,
&adjacency_calculator,
adjacency_weight,
verbose,
) {
return tile;
}
// Stage 2: Fallback selection with reset usage tracker
if verbose {
println!("⚠️ Using fallback tile selection with reset usage tracker...");
}
usage_tracker.reset();
if let Some(tile) = self.find_best_tile_with_constraints(
&target_lab_color,
&material_colors,
&kdtree,
&mut usage_tracker,
position,
&adjacency_calculator,
adjacency_weight,
verbose,
) {
return tile;
}
// Stage 3: Final fallback - best color match only
if verbose {
println!("⚠️ Using final fallback - best color match without adjacency constraints...");
}
self.find_best_tile_simple(&target_lab_color, &material_colors, &kdtree, verbose)
Algorithm Details
Stage 1 - Primary Selection:
- Uses k-d tree for O(log n) nearest neighbor search in Lab color space
- Applies usage limits through
UsageTracker
- Calculates adjacency penalties using
AdjacencyPenaltyCalculator
- Selects best tile considering all constraints
Stage 2 - Fallback Selection:
- Resets usage tracker to allow tile reuse
- Maintains adjacency constraints to prevent clustering
- Provides second chance for tiles that were previously exhausted
Stage 3 - Final Fallback:
- Ignores all constraints except color matching
- Guarantees a tile is selected for every grid position
- Uses simple Euclidean distance in Lab color space
Performance Impact
The fallback system has minimal performance overhead:
- Primary path: No additional cost for normal operation
- Fallback triggers: Only when necessary, affecting ~1-5% of tiles typically
- Final fallback: Extremely rare, used only in edge cases
- Memory usage: No additional memory allocation during fallback
Debugging and Monitoring
The system provides detailed logging when verbose mode is enabled:
- Tracks fallback activation frequency
- Reports usage tracker resets
- Logs adjacency constraint violations
- Provides insights for parameter tuning
Future Architecture Enhancements
Planned Improvements
- State Persistence: Save/load application settings
- Plugin Architecture: Extensible algorithm system
- Real-time Preview: Live mosaic preview during configuration
- Batch Processing: Multiple image processing queue
- Advanced Validation: Comprehensive input validation
Extensibility Points
- Message system can be extended for new features
- Settings structure supports backward-compatible additions
- Component system allows for new UI sections
- File dialog system can support additional formats
This architecture provides a solid foundation for the GUI application while maintaining flexibility for future enhancements and ensuring robust, testable code.