Analysis Date: 2026-01-04 Analyzer: Claude Code (Architecture Study) Focus: Feasibility of central state store in Makepad framework Context: P2 State Management task & App Contributor System design
Finding: Traditional centralized state management (Redux, Zustand pattern) is NOT feasible in Makepad's architecture due to fundamental differences in widget ownership and borrow checking models.
Alternative: Shell-coordinated state pattern IS feasible and aligns with Makepad's design philosophy.
App Contributor System: ✅ Already well-designed with current MofaApp trait. Minor enhancements recommended.
Recommendation: Skip P2 "Store" implementation. Use shell as state coordinator instead.
Makepad follows traditional desktop UI patterns (Win32, WxWidgets, Flutter) rather than modern web frameworks:
┌─────────────────────────────────────────────────────────────────────┐
│ WEB FRAMEWORKS (React/Redux) │
├─────────────────────────────────────────────────────────────────────┤
│ • UI = f(state) - Pure function transformation │
│ • Centralized store (Redux, Zustand, Jotai) │
│ • Props flow down, actions flow up │
│ • Components are functions, hooks for effects │
│ • Shared state via context/selectors │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ MAKEPAD FRAMEWORK │
├─────────────────────────────────────────────────────────────────────┤
│ • Widgets own their state locally │
│ • No global store by design │
│ • Parent controls child via WidgetRef methods │
│ • Components are structs with owned data │
│ • Borrow checking enforces single mutable reference │
│ • State lifetime = Widget lifetime │
└─────────────────────────────────────────────────────────────────────┘
Key Difference:
- Web frameworks: State exists outside components, flows in
- Makepad: State exists inside components, accessed via methods
Each widget owns its state as struct fields:
#[derive(Live, LiveHook, Widget)]
pub struct MoFaFMScreen {
#[deref]
view: View, // Composed child widgets
#[rust]
log_panel_collapsed: bool, // ← OWNED by this widget
#[rust]
audio_manager: Option<AudioManager>, // ← OWNED by this widget
#[rust]
log_entries: Vec<String>, // ← OWNED by this widget
}State Invariants:
- State lives inside the widget struct as
#[rust]fields - State lifetime is tied to widget lifetime
- No
Arc<Mutex<T>>orRwLock<T>- no shared ownership - Parent widgets own children via
WidgetReffields - Makepad's runtime borrow checker enforces single mutable reference
Makepad generates WidgetRef types for safe access across widget boundaries:
// Auto-generated by Makepad's derive macro
impl MoFaFMScreenRef {
pub fn stop_timers(&self, cx: &mut Cx) {
if let Some(inner) = self.borrow_mut() {
cx.stop_timer(inner.audio_timer);
}
}
pub fn add_log_entry(&self, cx: &mut Cx, entry: String) {
if let Some(mut inner) = self.borrow_mut() {
inner.log_entries.push(entry);
inner.view.redraw(cx);
}
}
pub fn update_dark_mode(&self, cx: &mut Cx, dark_mode: f64) {
if let Some(mut inner) = self.borrow_mut() {
inner.view.apply_over(cx, live!{
draw_bg: { dark_mode: (dark_mode) }
});
inner.view.redraw(cx);
}
}
}WidgetRef Characteristics:
- Smart pointer to the actual widget
borrow_mut()provides runtime borrow-checked mutable access- Methods on
WidgetRefform the public API for external control - Shell calls these methods to coordinate child widgets
// mofa-widgets/src/app_trait.rs
pub trait MofaApp {
fn info() -> AppInfo where Self: Sized;
fn live_design(cx: &mut Cx);
}
pub trait TimerControl {
fn stop_timers(&self, cx: &mut Cx);
fn start_timers(&self, cx: &mut Cx);
}
pub struct AppRegistry {
apps: Vec<AppInfo>, // Metadata only, no state
}Strengths:
- ✅ Standardized metadata (name, id, description)
- ✅ Consistent registration pattern
- ✅ Timer lifecycle control for resource management
- ✅ Works within Makepad's constraints
- ✅ Allows compile-time type safety
Limitations:
⚠️ Apps require compile-time imports in shell (Makepad limitation)⚠️ No inter-app communication mechanism⚠️ No shared state between apps (by design)
Coupling Point 1: Compile-time Imports (REQUIRED by Makepad)
// mofa-studio-shell/src/app.rs
use mofa_fm::screen::MoFaFMScreen; // ← MUST import concrete type
use mofa_settings::screen::SettingsScreen; // ← MUST import concrete type
live_design! {
fm_page = <MoFaFMScreen> {} // ← Concrete type required in DSL
}Why this is required:
live_design!macro is evaluated at compile time- Macro expansion needs concrete types for code generation
- No trait objects or dynamic dispatch in DSL
- This is a fundamental limitation of Makepad, not a design choice
Coupling Point 2: Direct Method Calls
// Shell controls apps via WidgetRef methods
impl App {
fn stop_fm_timers(&mut self, cx: &mut Cx) {
self.ui.mo_fa_fmscreen(ids!(fm_page)).stop_timers(cx);
}
fn apply_dark_mode_screens(&mut self, cx: &mut Cx) {
self.ui.mo_fa_fmscreen(ids!(fm_page))
.update_dark_mode(cx, self.dark_mode_anim);
self.ui.settings_screen(ids!(settings_page))
.update_dark_mode(cx, self.dark_mode_anim);
}
}Coupling Point 3: Event Interception
// Shell can intercept child widget events
impl App {
fn handle_mofa_hero_buttons(&mut self, cx: &mut Cx, event: &Event) {
let start_btn = self.ui.view(ids!(fm_page.mofa_hero.action_section.start_view));
match event.hits(cx, start_btn.area()) {
Hit::FingerUp(_) => {
// Handle start/stop logic at shell level
self.start_dataflow(cx);
}
_ => {}
}
}
}Makepad's widget tree uses runtime borrow checking that conflicts with shared mutable state:
// ❌ This pattern DOES NOT WORK in Makepad:
struct AppState {
data: Arc<Mutex<Vec<String>>>, // Try to share state
}
impl Widget {
fn update(&mut self, cx: &mut Cx) {
// Borrow conflict: Multiple widgets try to access shared state
let widget1 = self.ui.widget1(ids!(w1));
let widget2 = self.ui.widget2(ids!(w2));
widget1.borrow_mut().update(cx, &self.app_state); // Borrow 1
widget2.borrow_mut().update(cx, &self.app_state); // Borrow 2 - CONFLICT!
}
}Why it fails:
WidgetRef::borrow_mut()is runtime borrow checked- Makepad tracks active borrows to prevent aliasing violations
- Overlapping mutable borrows across different widgets cause runtime panics
- No
Arc<Mutex<T>>can work around this - the borrow checker is at the framework level
Widgets are instantiated by Makepad's runtime, not user code:
// Makepad internally creates widgets:
let widget = MoFaFMScreen::new(cx); // You don't control this
// ❌ You CANNOT inject state:
// let widget = MoFaFMScreen::with_store(app_store);
// let widget = MoFaFMScreen::new_with_state(cx, shared_state);No constructor control:
- Widget creation happens in
live_design!macro expansion - No custom constructors or builder patterns
- No way to pass dependencies at creation time
- State must be set after creation via method calls
live_design! {
// ❌ Trait objects DON'T WORK:
apps: Vec<Box<dyn MofaApp>>,
// ❌ Dynamic dispatch DON'T WORK:
app = <dyn MofaAppWidget>,
// ❌ Generics DON'T WORK:
app = <AppWidget<dyn MofaApp>>,
// ✅ Only concrete types work:
fm_page = <MoFaFMScreen> {},
settings_page = <SettingsScreen> {},
}Why:
live_design!is a macro that generates Rust code- Macro expansion needs to know exact types for monomorphization
- No type erasure or dynamic dispatch in generated code
- This is by design for performance and code generation
// All apps read/write to disk
pub struct Preferences {
pub dark_mode: bool,
pub providers: Vec<Provider>,
}
impl Preferences {
pub fn load() -> Result<Self> {
// Read from ~/.dora/dashboard/preferences.json
}
pub fn save(&self) -> Result<()> {
// Write to ~/.dora/dashboard/preferences.json
}
}
// Each app independently:
let prefs = Preferences::load()?;
self.dark_mode = prefs.dark_mode;Pros:
- ✅ Already works
- ✅ Simple and reliable
- ✅ Durable across restarts
- ✅ No sharing conflicts
Cons:
⚠️ No real-time synchronization⚠️ Serialization overhead⚠️ File I/O latency
Verdict: Keep as durability layer, combine with other patterns
impl App {
#[rust]
app_state: AppState, // Shell owns all shared state
fn notify_dark_mode_change(&mut self, cx: &mut Cx) {
// Propagate to all children via WidgetRef methods
self.ui.mo_fa_fmscreen(ids!(fm_page))
.on_dark_mode_change(cx, self.app_state.dark_mode);
self.ui.settings_screen(ids!(settings_page))
.on_dark_mode_change(cx, self.app_state.dark_mode);
self.ui.sidebar(ids!(sidebar_menu))
.on_dark_mode_change(cx, self.app_state.dark_mode);
}
}
// Apps implement state change handlers:
impl MoFaFMScreenRef {
pub fn on_dark_mode_change(&self, cx: &mut Cx, enabled: bool) {
if let Some(mut inner) = self.borrow_mut() {
inner.dark_mode = enabled;
inner.view.redraw(cx);
}
}
}Pros:
- ✅ Single source of truth
- ✅ Type-safe (Rust's type system)
- ✅ Follows Makepad patterns
- ✅ Explicit data flow (easy to debug)
- ✅ No runtime locks or borrow conflicts
Cons:
⚠️ Manual wiring required⚠️ Shell becomes coordinator
Verdict: Best approach for Makepad architecture
pub enum AppEvent {
DarkModeChanged { enabled: bool },
ProviderAdded { provider: Provider },
ProviderRemoved { id: ProviderId },
DataflowStarted,
DataflowStopped,
}
impl App {
fn broadcast_event(&mut self, cx: &mut Cx, event: AppEvent) {
// Send to all interested widgets
self.ui.mo_fa_fmscreen(ids!(fm_page))
.handle_app_event(cx, &event);
self.ui.settings_screen(ids!(settings_page))
.handle_app_event(cx, &event);
}
}
// Apps implement event handler:
impl MoFaFMScreenRef {
pub fn handle_app_event(&self, cx: &mut Cx, event: &AppEvent) {
match event {
AppEvent::DarkModeChanged { enabled } => {
self.on_dark_mode_change(cx, *enabled);
}
_ => {} // Ignore irrelevant events
}
}
}Pros:
- ✅ Decoupled (event emitter doesn't know receivers)
- ✅ Extensible (add new events without changing callers)
- ✅ Type-safe (enum variants)
Cons:
⚠️ More boilerplate⚠️ Still need manual wiring
Verdict: Good addition for complex interactions
// ❌ DOES NOT WORK in Makepad:
pub struct Store<T> {
state: Arc<RwLock<T>>,
reducers: Vec<Box<dyn Reducer<T>>>,
subscribers: Vec<Box<dyn Subscriber<T>>>,
}
impl<T> Store<T> {
pub fn dispatch(&self, action: Action) {
// Multiple widgets try to borrow simultaneously
let state = self.state.write().unwrap(); // Lock acquired
for subscriber in &self.subscribers {
subscriber.notify(&state); // Each subscriber borrows widget
// CONFLICT: WidgetRef::borrow_mut() while RwLock held
}
}
}Why it fails:
Arc<RwLock<T>>conflicts withWidgetRef::borrow_mut()- Lock ordering deadlocks (widget lock then store lock)
- Makepad's runtime borrow checker doesn't know about RwLock
- Nested borrowing causes runtime panics
// ❌ DOES NOT WORK in Makepad:
let state = use_store(cx); // No hooks system
impl Widget {
fn draw(&mut self, cx: &mut Cx) {
let dark_mode = self.state.dark_mode; // Can't access external state
// Widgets only have access to their own #[rust] fields
}
}Why it fails:
- No hooks system in Makepad
- Widgets can only access their own state
- No context or dependency injection
- State is owned, not referenced
// ❌ DOES NOT WORK in Makepad:
live_design! {
<AppStateProvider value={app_state}>
<MoFaFMScreen /> // Can't pass props like this
</AppStateProvider>
}Why it fails:
- No props system in
live_design! - No component composition with props
- Widgets aren't functions
- No HOC (Higher-Order Component) pattern
The current MofaApp trait system is well-designed:
// To add a new app, contributor creates:
// 1. App crate (apps/my_app/Cargo.toml)
[package]
name = "my-app"
// 2. App implementation (apps/my-app/src/lib.rs)
pub struct MyAppApp;
impl MofaApp for MyAppApp {
fn info() -> AppInfo {
AppInfo {
name: "My App",
id: "my-app",
description: "Does cool things",
}
}
fn live_design(cx: &mut Cx) {
screen::live_design(cx);
}
}
// 3. Shell modifications (3 lines of code)
// In mofa-studio-shell/src/app.rs imports:
use my_app::screen::MyAppScreen;
// In LiveRegister:
<MyAppApp as MofaApp>::live_design(cx);
// In after_new_from_doc:
self.app_registry.register(MyAppApp::info());
// In live_design!:
my_app_page = <MyAppScreen> {}Why this works well:
- ✅ Minimal shell changes (3-4 lines)
- ✅ No changes to app fundamentals
- ✅ Type-safe at compile time
- ✅ Works within Makepad constraints
- ✅ Clear contributor workflow
pub trait MofaApp {
fn info() -> AppInfo where Self: Sized;
fn live_design(cx: &mut Cx);
// NEW: Declare what state this app needs
fn state_dependencies() -> StateDependencies {
StateDependencies {
reads: vec!["dark_mode"],
writes: vec![],
}
}
}
pub struct StateDependencies {
pub reads: Vec<&'static str>,
pub writes: Vec<&'static str>,
}// Apps implement this if they need state change notifications
pub trait StateChangeListener {
fn on_dark_mode_change(&mut self, cx: &mut Cx, enabled: bool);
fn on_provider_change(&mut self, cx: &mut Cx, providers: &[Provider]);
}
// WidgetRef implementation
impl StateChangeListener for MoFaFMScreen {
fn on_dark_mode_change(&mut self, cx: &mut Cx, enabled: bool) {
self.dark_mode = enabled;
self.update_ui(cx);
}
}With enhancements, adding an app requires:
- ✅ Create struct implementing
MofaApp - ✅ Add 3 lines to shell (imports, registration, live_design)
- ➕ Optionally declare state dependencies
- ➕ Optionally implement state change listeners
NO changes to:
- ❌ App fundamentals
- ❌ Widget ownership model
- ❌ State management architecture
- ❌ Makepad framework constraints
Recommendation: DON'T implement traditional state store
| Pattern | Feasibility | Effort | Recommendation |
|---|---|---|---|
| Redux-style store | ❌ Not feasible | High | Don't pursue |
| Zustand-style hooks | ❌ Not supported | High | Don't pursue |
Arc<Mutex<T>> sharing |
❌ Conflicts with borrow checker | Medium | Don't pursue |
| Context/Provider | ❌ No props system | High | Don't pursue |
| Shell coordinator | ✅ Works | Low | Do this |
| File-based | ✅ Already done | None | Keep as-is |
| Event bus | ✅ Works | Medium | Optional |
Recommended Implementation:
// 1. Shell owns shared state
impl App {
#[rust]
app_state: AppState,
}
pub struct AppState {
pub dark_mode: bool,
pub providers: Vec<Provider>,
pub active_dataflow: bool,
// Add more as needed
}
// 2. Apps declare dependencies
impl MofaApp for MoFaFMApp {
fn state_dependencies() -> StateDependencies {
StateDependencies {
reads: vec!["dark_mode", "active_dataflow"],
writes: vec!["active_dataflow"],
}
}
}
// 3. Shell broadcasts changes
impl App {
fn notify_dark_mode_change(&mut self, cx: &mut Cx) {
// Use WidgetRef methods to notify children
self.broadcast_to_apps(cx, |app| {
app.on_dark_mode_change(cx, self.app_state.dark_mode);
});
}
}
// 4. File persistence for durability
impl AppState {
pub fn load() -> Self {
let prefs = Preferences::load().unwrap_or_default();
Self {
dark_mode: prefs.dark_mode,
providers: prefs.providers,
active_dataflow: false,
}
}
pub fn save(&self) {
let prefs = Preferences {
dark_mode: self.dark_mode,
providers: self.providers.clone(),
};
prefs.save().ok();
}
}Status: ✅ Already well-designed
Current MofaApp trait works well. Enhancements:
- ✅ Keep
MofaApptrait as-is - simple, effective - ✅ Keep
TimerControltrait - useful for lifecycle - ➕ Add
StateDependencies- let apps declare needs - ➕ Add
StateChangeListener- optional for notifications - ✅ Keep compile-time imports - Makepad limitation, accept it
| Criterion | Original | Updated | Status |
|---|---|---|---|
| Single source of truth for state | Central store | Shell coordinator | ✅ Feasible |
| Automatic UI updates | Subscribers | WidgetRef methods | ✅ Works |
| State persists across restarts | File I/O | File I/O | ✅ Already done |
| Contributors can add apps easily | Plugin system | MofaApp trait | ✅ Already works |
| Aspect | Web (React/Redux) | Makepad (Desktop UI) |
|---|---|---|
| State location | Central store | Component-owned |
| Data flow | Unidirectional (props down, actions up) | Parent↔Child methods |
| Mutation | Actions/Reducers | Direct method calls |
| Sharing | Context/Selectors | WidgetRef borrow_mut() |
| Lifecycle | Effect hooks | LiveHook traits |
| Paradigm | Functional transformations | Component ownership |
Makepad's architecture is intentional, not broken:
- Component ownership prevents bugs (no hidden global mutations)
- Borrow checking prevents race conditions at compile time
- WidgetRef provides safe cross-boundary access
- File persistence provides durability without complexity
- Explicit data flow makes debugging easier
Adding an app is already easy:
- Create struct implementing
MofaApp - Add 3-4 lines to shell
- No core refactoring needed
What contributors DON'T need:
- ❌ Understanding shell internals
- ❌ Modifying state management
- ❌ Learning complex patterns
- ❌ Breaking existing apps
What contributors DO need:
- ✅ Implement
MofaApptrait - ✅ Define widgets in
live_design! - ✅ Handle events in their widgets
- ✅ Optionally implement
TimerControl
Finding: Traditional centralized state management is architecturally incompatible with Makepad's component ownership model.
Alternatives that work:
- ✅ Shell as state coordinator (primary)
- ✅ File-based persistence (durability)
- ✅ Event broadcasting (optional, for complex cases)
What to avoid:
- ❌ Redux-style stores
- ❌ Shared mutable state (
Arc<Mutex<T>>) - ❌ Context/Provider patterns
- ❌ Dependency injection
Finding: Current MofaApp trait system is well-designed for contributors.
Minor enhancements recommended:
- State dependencies declaration
- State change listener trait (optional)
No major changes needed.
P2 State Management task: REVISED
Original plan:
- Design
Store<T>type ❌ - Implement subscribers ❌
- Add middleware ❌
Revised plan:
- ✅ Shell owns
AppStatestruct - ✅ Apps declare
StateDependencies - ✅ WidgetRef methods for state updates
- ✅ File persistence for durability
Timeline: 1-2 weeks (not 4-6 weeks)
- Add
AppStatestruct toApp - Implement shell coordinator pattern
- Document contributor workflow
- Add
StateDependenciestoMofaApptrait - Implement state change notifications
- Create contributor guide
-
Design- Not feasibleStore<T>type -
Implement action/reducer pattern- Not compatible -
Add middleware system- Not needed
// mofa-studio-shell/src/app.rs
impl App {
#[rust]
app_state: AppState,
fn toggle_dark_mode(&mut self, cx: &mut Cx) {
self.app_state.dark_mode = !self.app_state.dark_mode;
// Save to disk
self.app_state.save();
// Broadcast to all widgets
self.apply_dark_mode_panels(cx);
self.apply_dark_mode_screens(cx);
// Notify apps that implement StateChangeListener
self.notify_state_change(cx, StateEvent::DarkModeChanged {
enabled: self.app_state.dark_mode
});
}
fn notify_state_change(&mut self, cx: &mut Cx, event: StateEvent) {
// Notify interested apps
self.ui.mo_fa_fmscreen(ids!(fm_page))
.on_state_event(cx, &event);
self.ui.settings_screen(ids!(settings_page))
.on_state_event(cx, &event);
}
}// apps/my_app/src/lib.rs
pub struct MyAppApp;
impl MofaApp for MyAppApp {
fn info() -> AppInfo {
AppInfo {
name: "My App",
id: "my-app",
description: "Demonstrates state management",
}
}
fn live_design(cx: &mut Cx) {
screen::live_design(cx);
}
fn state_dependencies() -> StateDependencies {
StateDependencies {
reads: vec!["dark_mode", "providers"],
writes: vec![],
}
}
}// apps/my_app/src/screen.rs
impl MyAppScreenRef {
pub fn on_state_event(&self, cx: &mut Cx, event: &StateEvent) {
match event {
StateEvent::DarkModeChanged { enabled } => {
if let Some(mut inner) = self.borrow_mut() {
inner.dark_mode = *enabled;
inner.view.redraw(cx);
}
}
StateEvent::ProvidersChanged { providers } => {
if let Some(mut inner) = self.borrow_mut() {
inner.providers = providers.clone();
inner.view.redraw(cx);
}
}
_ => {}
}
}
}Similarities:
- Widgets own state
- Parent controls child via methods
- No global store
Differences:
- Flutter has InheritedWidget for state sharing
- Makepad has no equivalent (by design)
React:
- State outside components
- Hooks for effects
- Context for sharing
Makepad:
- State inside components
- LiveHook for lifecycle
- WidgetRef for access
Document Version: 1.0 Last Updated: 2026-01-04 Related Documents:
- CHECKLIST.md - Master roadmap
- roadmap-glm.md - Strategic planning
- APP_DEVELOPMENT_GUIDE.md - Contributor guide