// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Rendering;
///
/// Manages dirty rectangles for optimized rendering.
/// Only redraws areas that have been invalidated.
///
public class DirtyRectManager
{
private readonly List _dirtyRects = new();
private readonly object _lock = new();
private bool _fullRedrawNeeded = true;
private SKRect _bounds;
private int _maxDirtyRects = 10;
///
/// Gets or sets the maximum number of dirty rectangles to track before
/// falling back to a full redraw.
///
public int MaxDirtyRects
{
get => _maxDirtyRects;
set => _maxDirtyRects = Math.Max(1, value);
}
///
/// Gets whether a full redraw is needed.
///
public bool NeedsFullRedraw => _fullRedrawNeeded;
///
/// Gets the current dirty rectangles.
///
public IReadOnlyList DirtyRects
{
get
{
lock (_lock)
{
return _dirtyRects.ToList();
}
}
}
///
/// Gets whether there are any dirty regions.
///
public bool HasDirtyRegions
{
get
{
lock (_lock)
{
return _fullRedrawNeeded || _dirtyRects.Count > 0;
}
}
}
///
/// Sets the rendering bounds.
///
public void SetBounds(SKRect bounds)
{
if (_bounds != bounds)
{
_bounds = bounds;
InvalidateAll();
}
}
///
/// Invalidates a specific region.
///
public void Invalidate(SKRect rect)
{
if (rect.IsEmpty) return;
lock (_lock)
{
if (_fullRedrawNeeded) return;
// Clamp to bounds
rect = SKRect.Intersect(rect, _bounds);
if (rect.IsEmpty) return;
// Try to merge with existing dirty rects
for (int i = 0; i < _dirtyRects.Count; i++)
{
if (_dirtyRects[i].Contains(rect))
{
// Already covered
return;
}
if (rect.Contains(_dirtyRects[i]))
{
// New rect covers existing
_dirtyRects[i] = rect;
MergeDirtyRects();
return;
}
// Check if they overlap significantly (50% overlap)
var intersection = SKRect.Intersect(_dirtyRects[i], rect);
if (!intersection.IsEmpty)
{
float intersectArea = intersection.Width * intersection.Height;
float smallerArea = Math.Min(
_dirtyRects[i].Width * _dirtyRects[i].Height,
rect.Width * rect.Height);
if (intersectArea > smallerArea * 0.5f)
{
// Merge the rectangles
_dirtyRects[i] = SKRect.Union(_dirtyRects[i], rect);
MergeDirtyRects();
return;
}
}
}
// Add as new dirty rect
_dirtyRects.Add(rect);
// Check if we have too many dirty rects
if (_dirtyRects.Count > _maxDirtyRects)
{
// Fall back to full redraw
_fullRedrawNeeded = true;
_dirtyRects.Clear();
}
}
}
///
/// Invalidates the entire rendering area.
///
public void InvalidateAll()
{
lock (_lock)
{
_fullRedrawNeeded = true;
_dirtyRects.Clear();
}
}
///
/// Clears all dirty regions after rendering.
///
public void Clear()
{
lock (_lock)
{
_fullRedrawNeeded = false;
_dirtyRects.Clear();
}
}
///
/// Gets the combined dirty region as a single rectangle.
///
public SKRect GetCombinedDirtyRect()
{
lock (_lock)
{
if (_fullRedrawNeeded || _dirtyRects.Count == 0)
{
return _bounds;
}
var combined = _dirtyRects[0];
for (int i = 1; i < _dirtyRects.Count; i++)
{
combined = SKRect.Union(combined, _dirtyRects[i]);
}
return combined;
}
}
///
/// Applies dirty region clipping to a canvas.
///
public void ApplyClipping(SKCanvas canvas)
{
lock (_lock)
{
if (_fullRedrawNeeded || _dirtyRects.Count == 0)
{
// No clipping needed for full redraw
return;
}
// Create a path from all dirty rects
using var path = new SKPath();
foreach (var rect in _dirtyRects)
{
path.AddRect(rect);
}
canvas.ClipPath(path);
}
}
private void MergeDirtyRects()
{
// Simple merge pass - could be optimized
bool merged;
do
{
merged = false;
for (int i = 0; i < _dirtyRects.Count - 1; i++)
{
for (int j = i + 1; j < _dirtyRects.Count; j++)
{
var intersection = SKRect.Intersect(_dirtyRects[i], _dirtyRects[j]);
if (!intersection.IsEmpty)
{
_dirtyRects[i] = SKRect.Union(_dirtyRects[i], _dirtyRects[j]);
_dirtyRects.RemoveAt(j);
merged = true;
break;
}
}
if (merged) break;
}
} while (merged);
}
}