refactor: Rename DataManager to ChangeNotifier in panels

- Rename data_manager.zig to change_notifier.zig
- Update exports in panels.zig
- Clarifies that ChangeNotifier is for panel communication,
  not application data management (which apps implement separately)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-14 19:30:09 +01:00
parent 7f5550dd1f
commit 75613ec23f
3 changed files with 420 additions and 12 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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)