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.
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>,
}
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
}
}
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
}
}
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())
}
}