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)
|
||||
//! - 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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue