diff --git a/src/panels/change_notifier.zig b/src/panels/change_notifier.zig new file mode 100644 index 0000000..6f0894e --- /dev/null +++ b/src/panels/change_notifier.zig @@ -0,0 +1,408 @@ +//! ChangeNotifier - Observer pattern for panel communication +//! +//! Provides: +//! - Observer registration by entity type +//! - Notification of data changes +//! - Decoupled panel-to-panel communication +//! +//! Panels subscribe to entity types they care about and receive +//! notifications when data changes, without knowing about other panels. +//! +//! NOTE: This is NOT a data store. For application-specific data management +//! (BD access, caching, etc.), create a separate DataManager in your app. + +const std = @import("std"); + +// ============================================================================= +// Types +// ============================================================================= + +/// Change type +pub const ChangeType = enum { + /// Entity created + create, + /// Entity updated + update, + /// Entity deleted + delete, + /// Selection changed + select, + /// Refresh requested + refresh, +}; + +/// Data change event +pub const DataChange = struct { + /// Entity type (e.g., "Customer", "Document") + entity_type: []const u8, + /// Type of change + change_type: ChangeType, + /// Changed data (opaque pointer to entity) + data: ?*anyopaque = null, + /// Entity ID (if applicable) + entity_id: ?u64 = null, + /// Source panel ID (to avoid self-notification) + source_panel: ?[]const u8 = null, +}; + +/// Observer callback signature +pub const ObserverCallback = *const fn (change: DataChange, context: ?*anyopaque) void; + +/// Observer entry +pub const Observer = struct { + /// Callback function + callback: ObserverCallback, + /// User context + context: ?*anyopaque = null, + /// Observer ID (for removal) + id: []const u8 = "", +}; + +// ============================================================================= +// Data Manager +// ============================================================================= + +/// Maximum observers per entity type +pub const MAX_OBSERVERS_PER_TYPE = 32; +/// Maximum entity types +pub const MAX_ENTITY_TYPES = 64; + +/// ChangeNotifier - central hub for change notifications between panels +pub const ChangeNotifier = struct { + /// Observers by entity type (simple array-based for now) + entity_types: [MAX_ENTITY_TYPES]?[]const u8 = [_]?[]const u8{null} ** MAX_ENTITY_TYPES, + observers: [MAX_ENTITY_TYPES][MAX_OBSERVERS_PER_TYPE]?Observer = [_][MAX_OBSERVERS_PER_TYPE]?Observer{[_]?Observer{null} ** MAX_OBSERVERS_PER_TYPE} ** MAX_ENTITY_TYPES, + observer_counts: [MAX_ENTITY_TYPES]usize = [_]usize{0} ** MAX_ENTITY_TYPES, + entity_type_count: usize = 0, + + /// Global observers (receive all changes) + global_observers: [MAX_OBSERVERS_PER_TYPE]?Observer = [_]?Observer{null} ** MAX_OBSERVERS_PER_TYPE, + global_observer_count: usize = 0, + + const Self = @This(); + + /// Initialize data manager + pub fn init() Self { + return Self{}; + } + + /// Find entity type index (or create new) + fn findOrCreateEntityType(self: *Self, entity_type: []const u8) ?usize { + // Search existing + for (0..self.entity_type_count) |i| { + if (self.entity_types[i]) |et| { + if (std.mem.eql(u8, et, entity_type)) { + return i; + } + } + } + + // Create new + if (self.entity_type_count < MAX_ENTITY_TYPES) { + const idx = self.entity_type_count; + self.entity_types[idx] = entity_type; + self.entity_type_count += 1; + return idx; + } + + return null; + } + + /// Find entity type index + fn findEntityType(self: Self, entity_type: []const u8) ?usize { + for (0..self.entity_type_count) |i| { + if (self.entity_types[i]) |et| { + if (std.mem.eql(u8, et, entity_type)) { + return i; + } + } + } + return null; + } + + /// Add observer for specific entity type + pub fn addObserver(self: *Self, entity_type: []const u8, observer: Observer) bool { + const idx = self.findOrCreateEntityType(entity_type) orelse return false; + + if (self.observer_counts[idx] >= MAX_OBSERVERS_PER_TYPE) { + return false; + } + + self.observers[idx][self.observer_counts[idx]] = observer; + self.observer_counts[idx] += 1; + return true; + } + + /// Add global observer (receives all changes) + pub fn addGlobalObserver(self: *Self, observer: Observer) bool { + if (self.global_observer_count >= MAX_OBSERVERS_PER_TYPE) { + return false; + } + + self.global_observers[self.global_observer_count] = observer; + self.global_observer_count += 1; + return true; + } + + /// Remove observer by ID + pub fn removeObserver(self: *Self, entity_type: []const u8, observer_id: []const u8) bool { + const idx = self.findEntityType(entity_type) orelse return false; + + for (0..self.observer_counts[idx]) |i| { + if (self.observers[idx][i]) |obs| { + if (std.mem.eql(u8, obs.id, observer_id)) { + // Shift remaining observers + var j = i; + while (j < self.observer_counts[idx] - 1) : (j += 1) { + self.observers[idx][j] = self.observers[idx][j + 1]; + } + self.observers[idx][self.observer_counts[idx] - 1] = null; + self.observer_counts[idx] -= 1; + return true; + } + } + } + return false; + } + + /// Remove global observer by ID + pub fn removeGlobalObserver(self: *Self, observer_id: []const u8) bool { + for (0..self.global_observer_count) |i| { + if (self.global_observers[i]) |obs| { + if (std.mem.eql(u8, obs.id, observer_id)) { + // Shift remaining + var j = i; + while (j < self.global_observer_count - 1) : (j += 1) { + self.global_observers[j] = self.global_observers[j + 1]; + } + self.global_observers[self.global_observer_count - 1] = null; + self.global_observer_count -= 1; + return true; + } + } + } + return false; + } + + /// Notify observers of a change + pub fn notifyChange(self: *Self, change: DataChange) void { + // Notify type-specific observers + if (self.findEntityType(change.entity_type)) |idx| { + for (0..self.observer_counts[idx]) |i| { + if (self.observers[idx][i]) |obs| { + // Skip if this is the source panel + if (change.source_panel) |source| { + if (obs.id.len > 0 and std.mem.eql(u8, obs.id, source)) { + continue; + } + } + obs.callback(change, obs.context); + } + } + } + + // Notify global observers + for (0..self.global_observer_count) |i| { + if (self.global_observers[i]) |obs| { + if (change.source_panel) |source| { + if (obs.id.len > 0 and std.mem.eql(u8, obs.id, source)) { + continue; + } + } + obs.callback(change, obs.context); + } + } + } + + /// Convenience: notify entity update + pub fn notifyUpdate(self: *Self, entity_type: []const u8, data: ?*anyopaque) void { + self.notifyChange(.{ + .entity_type = entity_type, + .change_type = .update, + .data = data, + }); + } + + /// Convenience: notify entity selection + pub fn notifySelect(self: *Self, entity_type: []const u8, data: ?*anyopaque) void { + self.notifyChange(.{ + .entity_type = entity_type, + .change_type = .select, + .data = data, + }); + } + + /// Convenience: notify entity create + pub fn notifyCreate(self: *Self, entity_type: []const u8, data: ?*anyopaque) void { + self.notifyChange(.{ + .entity_type = entity_type, + .change_type = .create, + .data = data, + }); + } + + /// Convenience: notify entity delete + pub fn notifyDelete(self: *Self, entity_type: []const u8, entity_id: u64) void { + self.notifyChange(.{ + .entity_type = entity_type, + .change_type = .delete, + .entity_id = entity_id, + }); + } + + /// Convenience: request refresh + pub fn notifyRefresh(self: *Self, entity_type: []const u8) void { + self.notifyChange(.{ + .entity_type = entity_type, + .change_type = .refresh, + }); + } + + /// Get observer count for entity type + pub fn getObserverCount(self: Self, entity_type: []const u8) usize { + if (self.findEntityType(entity_type)) |idx| { + return self.observer_counts[idx]; + } + return 0; + } + + /// Check if has observers for entity type + pub fn hasObservers(self: Self, entity_type: []const u8) bool { + return self.getObserverCount(entity_type) > 0; + } +}; + +// ============================================================================= +// Global ChangeNotifier Instance +// ============================================================================= + +/// Global change notifier instance +var global_notifier: ?*ChangeNotifier = null; + +/// Get or create global change notifier +pub fn getChangeNotifier() *ChangeNotifier { + if (global_notifier) |n| { + return n; + } + // Note: In real usage, this should be properly allocated + // For now, using a static instance + const S = struct { + var instance: ChangeNotifier = ChangeNotifier.init(); + }; + global_notifier = &S.instance; + return &S.instance; +} + +/// Set global change notifier +pub fn setChangeNotifier(n: *ChangeNotifier) void { + global_notifier = n; +} + +// ============================================================================= +// Tests +// ============================================================================= + +test "ChangeNotifier basic" { + var dm = ChangeNotifier.init(); + + var received_count: usize = 0; + + const callback = struct { + fn cb(change: DataChange, context: ?*anyopaque) void { + _ = change; + if (context) |ctx| { + const count: *usize = @ptrCast(@alignCast(ctx)); + count.* += 1; + } + } + }.cb; + + // Add observer + try std.testing.expect(dm.addObserver("Customer", .{ + .callback = callback, + .context = &received_count, + .id = "test_observer", + })); + + try std.testing.expectEqual(@as(usize, 1), dm.getObserverCount("Customer")); + + // Notify + dm.notifyUpdate("Customer", null); + try std.testing.expectEqual(@as(usize, 1), received_count); + + // Different entity type - no notification + dm.notifyUpdate("Product", null); + try std.testing.expectEqual(@as(usize, 1), received_count); + + // Remove observer + try std.testing.expect(dm.removeObserver("Customer", "test_observer")); + try std.testing.expectEqual(@as(usize, 0), dm.getObserverCount("Customer")); +} + +test "ChangeNotifier global observer" { + var dm = ChangeNotifier.init(); + + var received_count: usize = 0; + + const callback = struct { + fn cb(change: DataChange, context: ?*anyopaque) void { + _ = change; + if (context) |ctx| { + const count: *usize = @ptrCast(@alignCast(ctx)); + count.* += 1; + } + } + }.cb; + + // Add global observer + try std.testing.expect(dm.addGlobalObserver(.{ + .callback = callback, + .context = &received_count, + .id = "global", + })); + + // Should receive all notifications + dm.notifyUpdate("Customer", null); + dm.notifyUpdate("Product", null); + dm.notifyUpdate("Order", null); + + try std.testing.expectEqual(@as(usize, 3), received_count); +} + +test "ChangeNotifier source panel skip" { + var dm = ChangeNotifier.init(); + + var received_count: usize = 0; + + const callback = struct { + fn cb(change: DataChange, context: ?*anyopaque) void { + _ = change; + if (context) |ctx| { + const count: *usize = @ptrCast(@alignCast(ctx)); + count.* += 1; + } + } + }.cb; + + _ = dm.addObserver("Customer", .{ + .callback = callback, + .context = &received_count, + .id = "panel_a", + }); + + _ = dm.addObserver("Customer", .{ + .callback = callback, + .context = &received_count, + .id = "panel_b", + }); + + // Notify with source - should skip panel_a + dm.notifyChange(.{ + .entity_type = "Customer", + .change_type = .update, + .source_panel = "panel_a", + }); + + // Only panel_b should receive + try std.testing.expectEqual(@as(usize, 1), received_count); +} diff --git a/src/panels/panel.zig b/src/panels/panel.zig index 4fd2e20..31a3283 100644 --- a/src/panels/panel.zig +++ b/src/panels/panel.zig @@ -4,7 +4,7 @@ //! - Each panel is autonomous (owns state, UI, logic) //! - Panels are reusable across windows //! - Windows compose panels (not inheritance) -//! - Communication via DataManager (observer pattern) +//! - Communication via ChangeNotifier (observer pattern) const std = @import("std"); const Context = @import("../core/context.zig").Context; diff --git a/src/panels/panels.zig b/src/panels/panels.zig index a88f566..b3518b4 100644 --- a/src/panels/panels.zig +++ b/src/panels/panels.zig @@ -3,14 +3,14 @@ //! This module provides: //! - AutonomousPanel: Self-contained UI component //! - Composite patterns: Vertical, Horizontal, Split, Tab, Grid -//! - DataManager: Observer pattern for panel communication +//! - ChangeNotifier: Observer pattern for panel communication //! - Detail: Components for detail/edit panels (state machine, semaphore, buttons) //! //! Architecture based on Simifactu's Lego Panels system: //! - Panels are autonomous (own state, UI, logic) //! - Panels are reusable across windows //! - Windows compose panels (not inheritance) -//! - Communication via DataManager (observer pattern) +//! - Communication via ChangeNotifier (observer pattern) const std = @import("std"); @@ -20,7 +20,7 @@ const std = @import("std"); pub const panel = @import("panel.zig"); pub const composite = @import("composite.zig"); -pub const data_manager = @import("data_manager.zig"); +pub const change_notifier = @import("change_notifier.zig"); pub const detail = @import("detail/detail.zig"); // ============================================================================= @@ -49,16 +49,16 @@ pub const TabState = composite.TabState; pub const GridComposite = composite.GridComposite; // ============================================================================= -// Data Manager types +// ChangeNotifier types (for panel-to-panel communication) // ============================================================================= -pub const DataManager = data_manager.DataManager; -pub const DataChange = data_manager.DataChange; -pub const ChangeType = data_manager.ChangeType; -pub const Observer = data_manager.Observer; -pub const ObserverCallback = data_manager.ObserverCallback; -pub const getDataManager = data_manager.getDataManager; -pub const setDataManager = data_manager.setDataManager; +pub const ChangeNotifier = change_notifier.ChangeNotifier; +pub const DataChange = change_notifier.DataChange; +pub const ChangeType = change_notifier.ChangeType; +pub const Observer = change_notifier.Observer; +pub const ObserverCallback = change_notifier.ObserverCallback; +pub const getChangeNotifier = change_notifier.getChangeNotifier; +pub const setChangeNotifier = change_notifier.setChangeNotifier; // ============================================================================= // Detail Panel types (for edit/detail panels)