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:
parent
7f5550dd1f
commit
75613ec23f
3 changed files with 420 additions and 12 deletions
408
src/panels/change_notifier.zig
Normal file
408
src/panels/change_notifier.zig
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! - Each panel is autonomous (owns state, UI, logic)
|
//! - Each panel is autonomous (owns state, UI, logic)
|
||||||
//! - Panels are reusable across windows
|
//! - Panels are reusable across windows
|
||||||
//! - Windows compose panels (not inheritance)
|
//! - Windows compose panels (not inheritance)
|
||||||
//! - Communication via DataManager (observer pattern)
|
//! - Communication via ChangeNotifier (observer pattern)
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Context = @import("../core/context.zig").Context;
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
//! This module provides:
|
//! This module provides:
|
||||||
//! - AutonomousPanel: Self-contained UI component
|
//! - AutonomousPanel: Self-contained UI component
|
||||||
//! - Composite patterns: Vertical, Horizontal, Split, Tab, Grid
|
//! - 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)
|
//! - Detail: Components for detail/edit panels (state machine, semaphore, buttons)
|
||||||
//!
|
//!
|
||||||
//! Architecture based on Simifactu's Lego Panels system:
|
//! Architecture based on Simifactu's Lego Panels system:
|
||||||
//! - Panels are autonomous (own state, UI, logic)
|
//! - Panels are autonomous (own state, UI, logic)
|
||||||
//! - Panels are reusable across windows
|
//! - Panels are reusable across windows
|
||||||
//! - Windows compose panels (not inheritance)
|
//! - Windows compose panels (not inheritance)
|
||||||
//! - Communication via DataManager (observer pattern)
|
//! - Communication via ChangeNotifier (observer pattern)
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ const std = @import("std");
|
||||||
|
|
||||||
pub const panel = @import("panel.zig");
|
pub const panel = @import("panel.zig");
|
||||||
pub const composite = @import("composite.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");
|
pub const detail = @import("detail/detail.zig");
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -49,16 +49,16 @@ pub const TabState = composite.TabState;
|
||||||
pub const GridComposite = composite.GridComposite;
|
pub const GridComposite = composite.GridComposite;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Data Manager types
|
// ChangeNotifier types (for panel-to-panel communication)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
pub const DataManager = data_manager.DataManager;
|
pub const ChangeNotifier = change_notifier.ChangeNotifier;
|
||||||
pub const DataChange = data_manager.DataChange;
|
pub const DataChange = change_notifier.DataChange;
|
||||||
pub const ChangeType = data_manager.ChangeType;
|
pub const ChangeType = change_notifier.ChangeType;
|
||||||
pub const Observer = data_manager.Observer;
|
pub const Observer = change_notifier.Observer;
|
||||||
pub const ObserverCallback = data_manager.ObserverCallback;
|
pub const ObserverCallback = change_notifier.ObserverCallback;
|
||||||
pub const getDataManager = data_manager.getDataManager;
|
pub const getChangeNotifier = change_notifier.getChangeNotifier;
|
||||||
pub const setDataManager = data_manager.setDataManager;
|
pub const setChangeNotifier = change_notifier.setChangeNotifier;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Detail Panel types (for edit/detail panels)
|
// Detail Panel types (for edit/detail panels)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue