diff --git a/src/core/context.zig b/src/core/context.zig index 903bd6d..3d9f0db 100644 --- a/src/core/context.zig +++ b/src/core/context.zig @@ -87,6 +87,16 @@ pub const Context = struct { /// Whether the entire screen needs redraw full_redraw: bool, + // ========================================================================= + // Dirty Panel System (granularity per named panel) + // ========================================================================= + + /// Registered panel areas by name (e.g., "who_list", "doc_detail") + panel_areas: std.StringHashMapUnmanaged(Layout.Rect), + + /// Dirty flags per panel (true = needs redraw) + dirty_panels: std.StringHashMapUnmanaged(bool), + /// Frame statistics stats: FrameStats, @@ -157,6 +167,8 @@ pub const Context = struct { .height = height, .dirty_rects = .{}, .full_redraw = true, + .panel_areas = .{}, + .dirty_panels = .{}, .stats = .{}, .focus = FocusSystem.init(), }; @@ -177,6 +189,8 @@ pub const Context = struct { .height = height, .dirty_rects = .{}, .full_redraw = true, + .panel_areas = .{}, + .dirty_panels = .{}, .stats = .{}, .focus = FocusSystem.init(), }; @@ -188,6 +202,8 @@ pub const Context = struct { self.overlay_commands.deinit(self.allocator); self.id_stack.deinit(self.allocator); self.dirty_rects.deinit(self.allocator); + self.panel_areas.deinit(self.allocator); + self.dirty_panels.deinit(self.allocator); self.frame_arena.deinit(); } @@ -238,6 +254,10 @@ pub const Context = struct { // Reset full_redraw for next frame self.full_redraw = false; + + // Clear dirty panel flags AFTER rendering + // (renderer uses getDirtyPanelRects during frame) + self.clearDirtyPanels(); } // ========================================================================= @@ -743,6 +763,132 @@ pub const Context = struct { self.width = width; self.height = height; self.invalidateAll(); + self.markAllPanelsDirty(); // Resize requires all panels to redraw + } + + // ========================================================================= + // Dirty Panel System (granularity per named panel) + // + // The application registers panel areas at startup: + // ctx.registerPanelArea("who_list", rect); + // + // When data changes, the application marks panels dirty: + // ctx.invalidatePanel("who_list"); + // + // The renderer checks which panels are dirty: + // const dirty_rects = ctx.getDirtyPanelRects(); + // renderer.clearDirtyRegions(dirty_rects); + // + // At endFrame(), dirty flags are cleared automatically. + // ========================================================================= + + /// Register a named panel area. Call at startup or when layout changes. + /// The ID should be descriptive (e.g., "who_list", "doc_detail"). + pub fn registerPanelArea(self: *Self, id: []const u8, rect: Layout.Rect) void { + self.panel_areas.put(self.allocator, id, rect) catch { + // If we can't register, fall back to full redraw + self.full_redraw = true; + }; + // New panels start dirty (need initial draw) + self.dirty_panels.put(self.allocator, id, true) catch {}; + } + + /// Update a panel's rect (e.g., after window resize). + /// Does NOT mark the panel dirty - call invalidatePanel separately if needed. + pub fn updatePanelArea(self: *Self, id: []const u8, rect: Layout.Rect) void { + if (self.panel_areas.getPtr(id)) |ptr| { + ptr.* = rect; + } + } + + /// Mark a panel as needing redraw. + /// Call this when data changes that affects the panel's content. + pub fn invalidatePanel(self: *Self, id: []const u8) void { + if (self.dirty_panels.getPtr(id)) |ptr| { + ptr.* = true; + } else { + // Panel not registered - this is probably a bug + // For safety, do full redraw + self.full_redraw = true; + } + } + + /// Check if a panel is marked dirty. + pub fn isPanelDirty(self: *Self, id: []const u8) bool { + if (self.full_redraw) return true; + return self.dirty_panels.get(id) orelse false; + } + + /// Mark all registered panels as dirty. + /// Use for resize, tab change, or other global invalidation. + pub fn markAllPanelsDirty(self: *Self) void { + var iter = self.dirty_panels.iterator(); + while (iter.next()) |entry| { + entry.value_ptr.* = true; + } + } + + /// Get rectangles of all dirty panels (for renderer). + /// Returns slice allocated from frame arena (valid until next beginFrame). + pub fn getDirtyPanelRects(self: *Self) []const Layout.Rect { + if (self.full_redraw) { + // Return single rect covering entire screen + const full = Layout.Rect{ + .x = 0, + .y = 0, + .w = self.width, + .h = self.height, + }; + const result = self.frame_arena.alloc_slice(Layout.Rect, 1) orelse return &.{}; + result[0] = full; + return result; + } + + // Count dirty panels + var dirty_count: usize = 0; + var iter = self.dirty_panels.iterator(); + while (iter.next()) |entry| { + if (entry.value_ptr.*) dirty_count += 1; + } + + if (dirty_count == 0) return &.{}; + + // Allocate result array from frame arena + const result = self.frame_arena.alloc_slice(Layout.Rect, dirty_count) orelse return &.{}; + + // Fill with dirty panel rects + var i: usize = 0; + var iter2 = self.dirty_panels.iterator(); + while (iter2.next()) |entry| { + if (entry.value_ptr.*) { + if (self.panel_areas.get(entry.key_ptr.*)) |rect| { + result[i] = rect; + i += 1; + } + } + } + + return result[0..i]; + } + + /// Check if any panel is dirty (useful for skip-redraw optimization). + pub fn hasAnyDirtyPanel(self: *Self) bool { + if (self.full_redraw) return true; + + var iter = self.dirty_panels.iterator(); + while (iter.next()) |entry| { + if (entry.value_ptr.*) return true; + } + return false; + } + + /// Clear all dirty panel flags. + /// Called automatically at endFrame(), but can be called manually if needed. + pub fn clearDirtyPanels(self: *Self) void { + var iter = self.dirty_panels.iterator(); + while (iter.next()) |entry| { + entry.value_ptr.* = false; + } } // =========================================================================