Status: Design Complete (Implementation Pending) Version: 1.0 Last Updated: 2026-02-17
This document describes the component-based docking architecture for rusty-app, a flexible system where UI components (Servers, Tables, Properties, Editor) can be registered as "Views" and docked in application regions (LeftPanel, MainPanel). Views can be toggled via the View menu with checkmarks indicating enabled state. Only enabled views appear as tabs in their docked region.
- Dynamic UI Composition: Components can be enabled/disabled at runtime
- Type-Safe Abstraction: Enum-based dispatch eliminates string-based lookups
- Extensible Design: Adding new components or regions requires minimal changes
- Separation of Concerns: Clear boundaries between data state and UI state
- Iced-Friendly: Leverages Iced's Elm-inspired architecture
The architecture consists of four layers, each with a distinct responsibility:
┌─────────────────────────────────────────────────────────────┐
│ Layer 4: Region (LeftPanelRegion, MainPanelRegion) │
│ - Dockable areas that host components │
│ - Manage active tab state (UI state) │
│ - Query ViewRegistry for enabled views │
│ - Render tabs and delegate to active component │
└─────────────────────────────────────────────────────────────┘
↓ queries
┌─────────────────────────────────────────────────────────────┐
│ Layer 3: ViewRegistry │
│ - Centralized registry (single source of truth) │
│ - HashMap<ComponentId, View> for O(1) operations │
│ - Enable/disable/toggle operations │
│ - Query enabled views per region │
└─────────────────────────────────────────────────────────────┘
↓ owns
┌─────────────────────────────────────────────────────────────┐
│ Layer 2: View │
│ - Wraps Component with enabled state │
│ - Associates component with RegionId │
│ - Default enabled: true │
└─────────────────────────────────────────────────────────────┘
↓ wraps
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Component trait (ServerList, TableList, etc.) │
│ - Stateless UI components │
│ - Implement view() method for rendering │
│ - Emit ComponentAction via callback │
└─────────────────────────────────────────────────────────────┘
| Layer | Purpose | State | Example |
|---|---|---|---|
| Component | Render UI | None (stateless) | ServerListComponent.view() renders server list |
| View | Add visibility toggle | enabled: bool, region: RegionId |
View wraps ServerList, enabled=true, region=LeftPanel |
| ViewRegistry | Manage enabled state | HashMap<ComponentId, View> |
Registry tracks which views are enabled |
| Region | Layout & composition | active_tab: Option<ComponentId> |
LeftPanelRegion queries registry, renders tabs |
Type-safe identifier for each component.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ComponentId {
ServerList,
TableList,
Properties,
Editor,
ConnectionForm,
}
impl ComponentId {
pub fn all() -> &'static [ComponentId] {
&[
ComponentId::ServerList,
ComponentId::TableList,
ComponentId::Properties,
ComponentId::Editor,
ComponentId::ConnectionForm,
]
}
pub fn name(&self) -> &'static str {
match self {
ComponentId::ServerList => "Servers",
ComponentId::TableList => "Tables",
ComponentId::Properties => "Properties",
ComponentId::Editor => "Query Editor",
ComponentId::ConnectionForm => "Connection Manager",
}
}
}Design Rationale:
- Enum over strings: Compile-time safety, no typos, exhaustive matching
- Copy + Hash: Efficient HashMap keys, no allocations
- Centralized names: UI consistency guaranteed
Unified action type with nested component-specific actions.
#[derive(Debug, Clone)]
pub enum ComponentAction {
ServerList(ServerListAction),
TableList(TableListAction),
Properties(PropertiesAction),
Editor(EditorAction),
ConnectionForm(ConnectionFormAction),
}
// Example component-specific actions
#[derive(Debug, Clone)]
pub enum ServerListAction {
SelectServer(String),
NewConnection,
RefreshList,
}
#[derive(Debug, Clone)]
pub enum EditorAction {
NewTab,
CloseTab(usize),
SelectTab(usize),
QueryChanged(String),
ExecuteQuery,
}Design Rationale:
- Nested enums: Each component has isolated action space, no naming conflicts
- Clone: Required for Iced message handling
- Extensible: Adding new components adds one variant
Stateless UI components with generic view method.
use iced::Element;
use crate::theme::ThemeColors;
pub trait Component: Send {
/// Unique identifier for this component
fn id(&self) -> ComponentId;
/// Display name for tabs and menus
fn title(&self) -> &str {
self.id().name()
}
/// Render the component with generic message mapping
fn view<'a, Message: 'a + Clone>(
&'a self,
theme: ThemeColors,
on_action: impl Fn(ComponentAction) -> Message + 'a,
) -> Element<'a, Message>;
}Design Rationale:
- Stateless: State lives in DatabaseIDE, not in components
- Send bound: Required for
Box<dyn Component>trait objects - Generic view(): Allows composition into any Message type
- Callback pattern: Component emits actions, caller maps to messages
Type-safe region identification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RegionId {
LeftPanel,
MainPanel,
}
impl RegionId {
pub fn all() -> &'static [RegionId] {
&[RegionId::LeftPanel, RegionId::MainPanel]
}
pub fn name(&self) -> &'static str {
match self {
RegionId::LeftPanel => "Left Panel",
RegionId::MainPanel => "Main Panel",
}
}
}Design Rationale:
- Enum for type safety: No string-based region lookups
- Copy + Hash: Efficient for HashMap keys
- Extensible: Future regions (RightPanel, BottomPanel) just add variants
Component wrapper with visibility state.
pub struct View {
component: Box<dyn Component>,
enabled: bool,
region: RegionId,
}
impl View {
pub fn new(component: Box<dyn Component>, region: RegionId) -> Self {
Self {
component,
enabled: true, // Default to enabled
region,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
pub fn region(&self) -> RegionId {
self.region
}
pub fn component(&self) -> &dyn Component {
self.component.as_ref()
}
}Design Rationale:
- Wraps Box: Dynamic dispatch for trait objects
- enabled: bool: Toggle-able visibility state
- region: RegionId: Fixed region assignment per view
- Default enabled: true: Components visible by default
Centralized view management with O(1) operations.
use std::collections::HashMap;
pub struct ViewRegistry {
views: HashMap<ComponentId, View>,
}
impl ViewRegistry {
pub fn new() -> Self {
Self {
views: HashMap::new(),
}
}
pub fn register(&mut self, component: Box<dyn Component>, region: RegionId) {
let id = component.id();
self.views.insert(id, View::new(component, region));
}
pub fn enable(&mut self, id: ComponentId) -> bool {
if let Some(view) = self.views.get_mut(&id) {
view.enable();
true
} else {
false
}
}
pub fn disable(&mut self, id: ComponentId) -> bool {
if let Some(view) = self.views.get_mut(&id) {
view.disable();
true
} else {
false
}
}
pub fn toggle(&mut self, id: ComponentId) -> bool {
if let Some(view) = self.views.get_mut(&id) {
view.toggle();
true
} else {
false
}
}
pub fn is_enabled(&self, id: ComponentId) -> bool {
self.views
.get(&id)
.map(|view| view.is_enabled())
.unwrap_or(false)
}
pub fn enabled_views(&self, region: RegionId) -> Vec<&View> {
self.views
.values()
.filter(|view| view.region() == region && view.is_enabled())
.collect()
}
pub fn region_views(&self, region: RegionId) -> Vec<&View> {
self.views
.values()
.filter(|view| view.region() == region)
.collect()
}
}Design Rationale:
- HashMap over Vec: O(1) lookups by ComponentId (vs O(n) linear search)
- Owns Views: Single source of truth for component state
- Returns Vec<&View>: Simple API, collection small (~10 views)
- enabled_views(): Filters by region AND enabled state
Polymorphic interface for dockable areas.
pub trait Region {
fn id(&self) -> RegionId;
fn active_component(&self) -> Option<ComponentId>;
fn set_active_component(&mut self, component_id: ComponentId);
fn view<'a, Message: 'a + Clone>(
&'a self,
registry: &'a ViewRegistry,
theme: ThemeColors,
on_action: impl Fn(ComponentAction) -> Message + 'a,
) -> Element<'a, Message>;
}Design Rationale:
- Trait over enum: Different regions have different state (width, visibility)
- active_component: UI state (which tab is selected)
- view() takes registry: Render-time queries ensure dynamic updates
- Generic Message: Allows composition into DatabaseIDE's message type
Tabbed region with resize and visibility.
pub struct LeftPanelRegion {
active_tab: Option<ComponentId>,
width: f32,
visible: bool,
}
impl LeftPanelRegion {
pub fn new() -> Self {
Self {
active_tab: None,
width: 250.0,
visible: true,
}
}
pub fn width(&self) -> f32 {
self.width
}
pub fn set_width(&mut self, width: f32) {
self.width = width.max(100.0).min(600.0);
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn toggle_visibility(&mut self) {
self.visible = !self.visible;
}
}
impl Region for LeftPanelRegion {
fn id(&self) -> RegionId {
RegionId::LeftPanel
}
fn active_component(&self) -> Option<ComponentId> {
self.active_tab
}
fn set_active_component(&mut self, component_id: ComponentId) {
self.active_tab = Some(component_id);
}
fn view<'a, Message: 'a + Clone>(
&'a self,
registry: &'a ViewRegistry,
theme: ThemeColors,
on_action: impl Fn(ComponentAction) -> Message + 'a,
) -> Element<'a, Message> {
if !self.visible {
return container(row![]).into();
}
let enabled_views = registry.enabled_views(RegionId::LeftPanel);
// Render tabs from enabled views
let tabs = render_tabs(enabled_views, self.active_tab, theme, |id| {
Message::SelectTab(RegionId::LeftPanel, id)
});
// Render active component content
let content = if let Some(active_id) = self.active_tab {
if let Some(view) = enabled_views.iter().find(|v| v.component().id() == active_id) {
view.component().view(theme, on_action)
} else {
container(text("No component selected")).into()
}
} else {
container(text("No component selected")).into()
};
container(column![tabs, content].spacing(0))
.width(self.width)
.into()
}
}Design Rationale:
- width: f32: Resize state for drag-to-resize
- visible: bool: Toggle-able via View menu
- Query enabled_views(): Dynamic tabs based on registry state
- Delegate rendering: Calls
view.component().view()
Tabbed region that fills available space.
pub struct MainPanelRegion {
active_tab: Option<ComponentId>,
}
impl MainPanelRegion {
pub fn new() -> Self {
Self {
active_tab: None,
}
}
}
impl Region for MainPanelRegion {
fn id(&self) -> RegionId {
RegionId::MainPanel
}
fn active_component(&self) -> Option<ComponentId> {
self.active_tab
}
fn set_active_component(&mut self, component_id: ComponentId) {
self.active_tab = Some(component_id);
}
fn view<'a, Message: 'a + Clone>(
&'a self,
registry: &'a ViewRegistry,
theme: ThemeColors,
on_action: impl Fn(ComponentAction) -> Message + 'a,
) -> Element<'a, Message> {
let enabled_views = registry.enabled_views(RegionId::MainPanel);
let tabs = render_tabs(enabled_views, self.active_tab, theme, |id| {
Message::SelectTab(RegionId::MainPanel, id)
});
let content = if let Some(active_id) = self.active_tab {
if let Some(view) = enabled_views.iter().find(|v| v.component().id() == active_id) {
view.component().view(theme, on_action)
} else {
container(text("No component selected")).into()
}
} else {
container(text("No component selected")).into()
};
container(column![tabs, content].spacing(0)).into()
}
}Design Rationale:
- No width management: Fills available space
- Always visible: No visibility toggle (main content area)
- Same tab pattern: Reuses render_tabs helper
Decision: Components do not store state; state lives in DatabaseIDE.
Rationale:
- Iced's architecture:
update()handles state,view()renders - Components are trait objects (
Box<dyn Component>) - Storing state in trait objects complicates lifetimes
- Centralized state easier to serialize/debug
- Matches Iced's "single state tree" pattern
Implication: Components receive all needed data via view() parameters
Decision: Use HashMap<ComponentId, View> instead of Vec<View>.
Rationale:
- O(1) lookup by ComponentId for enable/disable operations
- No linear search needed for queries
- ComponentId is Copy + Hash (perfect for HashMap key)
- Natural mapping: one view per component ID
Trade-off: Slightly more memory overhead, but typically <10 views
Decision: Use enums for ComponentId, ComponentAction, RegionId instead of strings.
Rationale:
- Compile-time safety (no typos)
- Exhaustive matching in match statements
- HashMap key with no allocations (Copy)
- Easier refactoring (find all references)
- Better IDE support
Alternative Rejected: String-based IDs (&'static str) - runtime errors, no exhaustive matching
Decision: Each View has a fixed RegionId (not dynamically re-assignable).
Rationale:
- Components are designed for specific regions
- ServerList naturally belongs in LeftPanel
- Editor naturally belongs in MainPanel
- Simplifies queries (no need to track region separately)
Future Extension: If drag-and-drop docking is added, could make region mutable via set_region()
Decision: Regions store active_tab: Option<ComponentId>, not ViewRegistry.
Rationale:
- Active tab is UI state (which tab is selected)
- Enabled state is data state (which components are available)
- Separation of concerns: ViewRegistry manages availability, Region manages selection
- Multiple regions can have different active tabs
Example:
// ViewRegistry: "Editor is enabled in MainPanel"
registry.is_enabled(ComponentId::Editor) // true
// MainPanelRegion: "Editor tab is currently active"
main_panel.active_tab // Some(ComponentId::Editor)Decision: Regions query ViewRegistry at render time, not during initialization.
Rationale:
- Enabled views can change dynamically (View menu toggles)
- Render-time queries ensure tabs always reflect current state
- Single source of truth (ViewRegistry)
- No synchronization issues
Performance: Query is fast (HashMap filter, O(n) where n = total components, typically <10)
Decision: view<Message>() instead of view() -> Element<RegionMessage>.
Rationale:
- Allows
Box<dyn Component>(trait object) - Same component can render in different message contexts
- Callback pattern familiar from existing code
- More flexible for composition
Alternative Rejected: Associated type type Message - cannot use trait objects
Decision: Use trait Region instead of enum RegionType.
Rationale:
- Each region has different state (LeftPanel has width, MainPanel doesn't)
- Extensible: adding new region types doesn't require modifying core enum
- Allows region-specific methods (e.g.,
set_width()only on LeftPanel) - Matches Rust best practices for polymorphism
Alternative Considered: enum RegionType { LeftPanel { width, visible, ... }, MainPanel { ... } } - less clean API
pub struct DatabaseIDE {
// Existing fields
theme: ThemeColors,
// View registry (centralized data state)
view_registry: ViewRegistry,
// Regions (UI state)
left_panel: LeftPanelRegion,
main_panel: MainPanelRegion,
}Key Points:
- ViewRegistry: Single source of truth for enabled state
- Regions: Concrete structs (not
Box<dyn Region>) - Each region manages its own active tab
pub enum Message {
// Existing variants
MenuAction(MenuAction),
ComponentAction(ComponentAction),
SelectTab(RegionId, ComponentId),
TogglePanel(RegionId),
ResizePanel(RegionId, f32),
}
impl DatabaseIDE {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::MenuAction(MenuAction::View(view_item)) => {
if let Some(component_id) = view_item.as_component_id() {
self.view_registry.toggle(component_id);
// Auto-switch if toggling off active view
if self.left_panel.active_component() == Some(component_id) {
let enabled = self.view_registry.enabled_views(RegionId::LeftPanel);
if let Some(first) = enabled.first() {
self.left_panel.set_active_component(first.component().id());
} else {
self.left_panel.set_active_component(None);
}
}
}
Task::none()
}
Message::SelectTab(region_id, component_id) => {
match region_id {
RegionId::LeftPanel => {
self.left_panel.set_active_component(component_id);
}
RegionId::MainPanel => {
self.main_panel.set_active_component(component_id);
}
}
Task::none()
}
Message::TogglePanel(RegionId::LeftPanel) => {
self.left_panel.toggle_visibility();
Task::none()
}
Message::ComponentAction(action) => {
// Route to appropriate handler based on action variant
match action {
ComponentAction::ServerList(server_action) => {
// Handle server list actions
}
ComponentAction::Editor(editor_action) => {
// Handle editor actions
}
// ... other components
}
Task::none()
}
// ... other messages
}
}
}impl DatabaseIDE {
pub fn new() -> Self {
let mut view_registry = ViewRegistry::new();
// Register components with regions
view_registry.register(
Box::new(ServerListComponent),
RegionId::LeftPanel
);
view_registry.register(
Box::new(TableListComponent),
RegionId::LeftPanel
);
view_registry.register(
Box::new(PropertiesComponent),
RegionId::LeftPanel
);
view_registry.register(
Box::new(EditorComponent),
RegionId::MainPanel
);
view_registry.register(
Box::new(ConnectionFormComponent),
RegionId::MainPanel
);
let mut left_panel = LeftPanelRegion::new();
let mut main_panel = MainPanelRegion::new();
// Initialize active tabs from enabled views
if let Some(first) = view_registry.enabled_views(RegionId::LeftPanel).first() {
left_panel.set_active_component(first.component().id());
}
if let Some(first) = view_registry.enabled_views(RegionId::MainPanel).first() {
main_panel.set_active_component(first.component().id());
}
Self {
view_registry,
left_panel,
main_panel,
// ... other fields
}
}
}impl DatabaseIDE {
fn view(&self) -> Element<Message> {
let menu_bar = MenuBar::new().view(
&self.theme,
|action| Message::MenuAction(action)
);
let left_panel_view = self.left_panel.view(
&self.view_registry,
self.theme,
|action| Message::ComponentAction(action)
);
let main_panel_view = self.main_panel.view(
&self.view_registry,
self.theme,
|action| Message::ComponentAction(action)
);
container(
column![
menu_bar,
row![
left_panel_view,
main_panel_view
]
]
)
.into()
}
}pub enum ViewMenuItem {
ToggleLeftPanel,
Servers,
Tables,
Properties,
QueryEditor,
ConnectionManager,
}
impl ViewMenuItem {
pub fn as_component_id(&self) -> Option<ComponentId> {
match self {
ViewMenuItem::ToggleLeftPanel => None,
ViewMenuItem::Servers => Some(ComponentId::ServerList),
ViewMenuItem::Tables => Some(ComponentId::TableList),
ViewMenuItem::Properties => Some(ComponentId::Properties),
ViewMenuItem::QueryEditor => Some(ComponentId::Editor),
ViewMenuItem::ConnectionManager => Some(ComponentId::ConnectionForm),
}
}
}
// In MenuBar::view() - View menu items
let view_menu = ViewMenuItem::all()
.iter()
.map(|item| {
if let Some(component_id) = item.as_component_id() {
let is_enabled = registry.is_enabled(component_id);
button(
row![
if is_enabled { "✓ " } else { " " },
text(item.name())
]
)
.on_press(Message::MenuAction(MenuAction::View(*item)))
} else {
// ToggleLeftPanel - no checkmark
button(text(item.name()))
.on_press(Message::MenuAction(MenuAction::View(*item)))
}
});1. DatabaseIDE::new()
├─ Create ViewRegistry
├─ Register all components with regions
│ ├─ ServerList → LeftPanel (enabled by default)
│ ├─ TableList → LeftPanel (enabled by default)
│ ├─ Properties → LeftPanel (enabled by default)
│ ├─ Editor → MainPanel (enabled by default)
│ └─ ConnectionForm → MainPanel (enabled by default)
├─ Create LeftPanelRegion
├─ Create MainPanelRegion
└─ Initialize active tabs from enabled views
1. User clicks View → Servers
↓
2. MenuAction::View(ViewMenuItem::Servers)
↓
3. DatabaseIDE::update()
├─ view_registry.toggle(ComponentId::ServerList)
├─ If ServerList was active_tab:
│ ├─ Query enabled_views(LeftPanel)
│ └─ Set active_tab to first remaining (or None)
└─ Trigger re-render
↓
4. DatabaseIDE::view()
├─ MenuBar renders checkmarks based on registry.is_enabled()
├─ LeftPanel.view() queries enabled_views(LeftPanel)
├─ Tabs rendered from enabled views only
└─ Active component's view() method called
1. User clicks "Tables" tab
↓
2. Message::SelectTab(RegionId::LeftPanel, ComponentId::TableList)
↓
3. DatabaseIDE::update()
└─ left_panel.set_active_component(ComponentId::TableList)
↓
4. DatabaseIDE::view()
├─ LeftPanel.view() queries enabled_views(LeftPanel)
├─ Renders tabs (Tables highlighted as active)
└─ Calls TableList.view() for content
1. User clicks "New Connection" in ServerList
↓
2. Component emits ComponentAction::ServerList(ServerListAction::NewConnection)
↓
3. Callback maps to Message::ComponentAction(...)
↓
4. DatabaseIDE::update()
└─ Match on ComponentAction::ServerList variant
└─ Handle NewConnection (e.g., open connection dialog)
Step 1: Add ComponentId variant
// In components/mod.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ComponentId {
ServerList,
TableList,
Properties,
Editor,
ConnectionForm,
QueryResults, // ← New component
}
impl ComponentId {
pub fn name(&self) -> &'static str {
match self {
// ... existing
ComponentId::QueryResults => "Query Results",
}
}
}Step 2: Add ComponentAction variant
// In components/mod.rs
#[derive(Debug, Clone)]
pub enum ComponentAction {
ServerList(ServerListAction),
TableList(TableListAction),
Properties(PropertiesAction),
Editor(EditorAction),
ConnectionForm(ConnectionFormAction),
QueryResults(QueryResultsAction), // ← New action
}
#[derive(Debug, Clone)]
pub enum QueryResultsAction {
ExportCsv,
CopyToClipboard,
SortColumn(usize),
}Step 3: Implement Component trait
// In components/query_results.rs
pub struct QueryResultsComponent;
impl Component for QueryResultsComponent {
fn id(&self) -> ComponentId {
ComponentId::QueryResults
}
fn view<'a, Message: 'a + Clone>(
&'a self,
theme: ThemeColors,
on_action: impl Fn(ComponentAction) -> Message + 'a,
) -> Element<'a, Message> {
// Component implementation
let export_button = button(text("Export CSV"))
.on_press(on_action(
ComponentAction::QueryResults(
QueryResultsAction::ExportCsv
)
));
container(column![export_button]).into()
}
}Step 4: Register in DatabaseIDE
// In main.rs or lib.rs (DatabaseIDE::new)
view_registry.register(
Box::new(QueryResultsComponent),
RegionId::MainPanel // Choose appropriate region
);Step 5: Add to View menu (optional)
// In menu_bar.rs
pub enum ViewMenuItem {
ToggleLeftPanel,
Servers,
Tables,
Properties,
QueryEditor,
ConnectionManager,
QueryResults, // ← New menu item
}
impl ViewMenuItem {
pub fn as_component_id(&self) -> Option<ComponentId> {
match self {
// ... existing
ViewMenuItem::QueryResults => Some(ComponentId::QueryResults),
}
}
}Step 6: Handle actions in update()
// In DatabaseIDE::update()
match message {
Message::ComponentAction(action) => {
match action {
// ... existing
ComponentAction::QueryResults(results_action) => {
match results_action {
QueryResultsAction::ExportCsv => {
// Handle export
}
QueryResultsAction::CopyToClipboard => {
// Handle copy
}
QueryResultsAction::SortColumn(col) => {
// Handle sort
}
}
}
}
Task::none()
}
}When adding a new component, ensure:
- ComponentId variant added
- ComponentId::name() returns display name
- ComponentAction variant added (if component has actions)
- Component-specific action enum defined (if needed)
- Component struct implements Component trait
- Component registered in DatabaseIDE::new()
- ViewMenuItem variant added (if toggle-able)
- ViewMenuItem::as_component_id() mapping added
- ComponentAction handling in DatabaseIDE::update()
rusty-app/src/
├── main.rs // Application entry, DatabaseIDE
├── lib.rs // Public API
├── theme.rs // ThemeColors
├── menu_bar.rs // MenuBar, ViewMenuItem, MenuAction
├── views.rs // View, ViewRegistry, RegionId
├── regions.rs // Region trait, LeftPanelRegion, MainPanelRegion
├── components/
│ ├── mod.rs // Component trait, ComponentId, ComponentAction
│ ├── server_list.rs // ServerListComponent, ServerListAction
│ ├── table_list.rs // TableListComponent, TableListAction
│ ├── properties.rs // PropertiesComponent, PropertiesAction
│ ├── editor.rs // EditorComponent, EditorAction
│ └── connection_form.rs // ConnectionFormComponent, ConnectionFormAction
Module Responsibilities:
| Module | Exports | Purpose |
|---|---|---|
views.rs |
View, ViewRegistry, RegionId | View management layer |
regions.rs |
Region trait, LeftPanelRegion, MainPanelRegion | Region implementations |
components/mod.rs |
Component trait, ComponentId, ComponentAction | Component abstraction |
components/*.rs |
Individual component implementations | Concrete components |
To support drag-and-drop docking in the future:
-
Make View.region mutable:
impl View { pub fn set_region(&mut self, region: RegionId) { self.region = region; } }
-
Add drag state to regions:
pub struct LeftPanelRegion { active_tab: Option<ComponentId>, dragging: Option<ComponentId>, // ... }
-
Add ViewRegistry method:
impl ViewRegistry { pub fn move_to_region(&mut self, id: ComponentId, new_region: RegionId) { if let Some(view) = self.views.get_mut(&id) { view.set_region(new_region); } } }
To support floating component windows:
-
Add RegionId variant:
pub enum RegionId { LeftPanel, MainPanel, FloatingWindow(usize), // Window ID }
-
Create FloatingWindowRegion:
pub struct FloatingWindowRegion { window_id: usize, component: ComponentId, position: (f32, f32), size: (f32, f32), }
To support saving/loading layouts:
-
Add serialization to View:
#[derive(Serialize, Deserialize)] pub struct ViewLayout { pub component_id: ComponentId, pub enabled: bool, pub region: RegionId, } impl ViewRegistry { pub fn export_layout(&self) -> Vec<ViewLayout> { ... } pub fn import_layout(&mut self, layout: Vec<ViewLayout>) { ... } }
-
Add region state serialization:
#[derive(Serialize, Deserialize)] pub struct RegionLayout { pub active_tab: Option<ComponentId>, pub width: Option<f32>, pub visible: Option<bool>, }
-
Separation of Concerns:
- Components: Stateless rendering
- Views: Toggle-able visibility
- ViewRegistry: Centralized state management
- Regions: Layout and composition
-
Type Safety:
- Enums for IDs (ComponentId, RegionId)
- Enum dispatch for actions (ComponentAction)
- No string-based lookups
-
Single Source of Truth:
- ViewRegistry owns enabled state
- Regions manage active tab state
- No duplicate state
-
Dynamic Updates:
- Render-time queries to ViewRegistry
- Tabs generated from enabled views
- View menu reflects current state
-
Extensibility:
- Adding components: add variants, implement trait
- Adding regions: implement Region trait
- Adding actions: extend ComponentAction enum
- Architecture designed (Tasks 5vf.1.1 - 5vf.1.5)
- View System Implementation (Feature 5vf.2)
- View Menu Integration (Feature 5vf.3)
- Dynamic Left Panel (Feature 5vf.4)
- Component Refactoring (Feature 5vf.5)
- Region System (Feature 5vf.6)
- CLAUDE.md: Project overview and development guidelines
- README.md: User-facing documentation
- Design Documents (in /tmp during discovery):
/tmp/iced_patterns_research.md: Pattern research/tmp/component_trait_design.md: Component trait specification/tmp/view_registry_design.md: View/ViewRegistry specification/tmp/region_trait_design.md: Region trait specification
Maintained by: Claude Code Architecture Version: 1.0 Epic: rusty-app-5vf (Component-Based Docking Architecture)