System Overview

This document outlines the architecture for a Tauri-based application where Rust extensions can safely define and manipulate Shadcn UI components in a React frontend through a well-designed API layer.

Key Requirements

  1. Safe Rust-to-React UI manipulation
  2. Consistent React + Shadcn component tree
  3. Natural and powerful developer experience
  4. Performance optimization across the Tauri IPC boundary

Architecture Components

1. Virtual DOM Bridge Layer

The core innovation is a Virtual DOM abstraction that lives in Rust and mirrors the React component tree.

// Core abstraction for UI components in Rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualNode {
    pub id: String,
    pub component_type: ComponentType,
    pub props: HashMap<String, Value>,
    pub children: Vec<VirtualNode>,
    pub event_handlers: Vec<EventHandlerId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComponentType {
    // Shadcn components mapped to Rust
    Button { variant: ButtonVariant },
    Input { input_type: InputType },
    Card,
    Dialog,
    Table,
    // ... all 50 Shadcn components

    // Custom extension components
    Custom { name: String, schema: ComponentSchema },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentSchema {
    pub props_schema: JsonSchema,
    pub required_props: Vec<String>,
    pub default_props: HashMap<String, Value>,
}

2. Extension API Layer

Extensions interact with UI through a high-level, type-safe API that feels natural to Rust developers.

pub trait UIExtension {
    fn id(&self) -> &str;
    fn initialize(&mut self, context: &mut ExtensionContext) -> Result<(), ExtensionError>;
    fn render(&self, context: &RenderContext) -> Result<VirtualNode, RenderError>;
    fn handle_event(&mut self, event: UIEvent, context: &mut ExtensionContext) -> Result<(), ExtensionError>;
}

pub struct ExtensionContext {
    ui_builder: UIBuilder,
    state_manager: StateManager,
    event_emitter: EventEmitter,
}

impl ExtensionContext {
    pub fn create_component<T: ShadcnComponent>(&mut self, props: T::Props) -> ComponentBuilder<T> {
        ComponentBuilder::new(self, props)
    }

    pub fn update_component(&mut self, id: &str, updater: impl Fn(&mut VirtualNode)) -> Result<(), UIError> {
        // Safe component updates with validation
    }

    pub fn emit_event(&mut self, event: UIEvent) -> Result<(), EventError> {
        // Type-safe event emission
    }
}

3. Type-Safe Component Builders

Each Shadcn component has a corresponding Rust builder with compile-time type safety.

pub trait ShadcnComponent {
    type Props: Serialize + DeserializeOwned + Default;
    const COMPONENT_NAME: &'static str;
}

pub struct Button;
impl ShadcnComponent for Button {
    type Props = ButtonProps;
    const COMPONENT_NAME: &'static str = "Button";
}

#[derive(Serialize, Deserialize, Default)]
pub struct ButtonProps {
    pub variant: Option<ButtonVariant>,
    pub size: Option<ButtonSize>,
    pub disabled: Option<bool>,
    pub class_name: Option<String>,
    pub on_click: Option<EventHandlerId>,
}

pub struct ComponentBuilder<T: ShadcnComponent> {
    node: VirtualNode,
    _phantom: PhantomData<T>,
}

impl ComponentBuilder<Button> {
    pub fn variant(mut self, variant: ButtonVariant) -> Self {
        self.set_prop("variant", variant);
        self
    }

    pub fn on_click<F>(mut self, handler: F) -> Self
    where
        F: Fn(ClickEvent) -> Result<(), ExtensionError> + Send + Sync + 'static
    {
        let handler_id = self.register_event_handler(Box::new(handler));
        self.set_prop("onClick", handler_id);
        self
    }

    pub fn child<C: Into<VirtualNode>>(mut self, child: C) -> Self {
        self.node.children.push(child.into());
        self
    }

    pub fn build(self) -> VirtualNode {
        self.node
    }
}

4. Example Extension Implementation

pub struct FileExplorerExtension {
    current_path: PathBuf,
    files: Vec<FileInfo>,
    selected_file: Option<String>,
}

impl UIExtension for FileExplorerExtension {
    fn id(&self) -> &str {
        "file_explorer"
    }

    fn render(&self, context: &RenderContext) -> Result<VirtualNode, RenderError> {
        let tree = context.create_component::<Card>(CardProps::default())
            .class_name("file-explorer")
            .child(
                context.create_component::<Input>(InputProps::default())
                    .placeholder("Search files...")
                    .on_change(|event| {
                        // Handle search input
                        Ok(())
                    })
                    .build()
            )
            .child(
                self.build_file_tree(context)?
            )
            .build();

        Ok(tree)
    }

    fn handle_event(&mut self, event: UIEvent, context: &mut ExtensionContext) -> Result<(), ExtensionError> {
        match event {
            UIEvent::FileClick { path } => {
                self.selected_file = Some(path.clone());
                context.emit_event(UIEvent::FileSelected { path })?;
            }
            _ => {}
        }
        Ok(())
    }
}

impl FileExplorerExtension {
    fn build_file_tree(&self, context: &RenderContext) -> Result<VirtualNode, RenderError> {
        let mut tree = context.create_component::<div>(DivProps::default())
            .class_name("file-tree");

        for file in &self.files {
            let file_item = context.create_component::<Button>(ButtonProps::default())
                .variant(ButtonVariant::Ghost)
                .class_name(if Some(&file.name) == self.selected_file.as_ref() {
                    "file-item selected"
                } else {
                    "file-item"
                })
                .on_click({
                    let file_path = file.path.clone();
                    move |_| {
                        // This closure captures the file path
                        Ok(())
                    }
                })
                .child(file.name.clone())
                .build();

            tree = tree.child(file_item);
        }

        Ok(tree.build())
    }
}