// ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; namespace RGB.NET.Core; /// /// /// /// Represents a RGB-surface containing multiple devices. /// public sealed class RGBSurface : AbstractBindable, IDisposable { #region Properties & Fields private readonly Stopwatch _deltaTimeCounter; private readonly IList _devices = []; private readonly IList _updateTriggers = []; private readonly List _ledGroups = []; /// /// Gets a readonly list containing all loaded . /// This collection should be locked when enumerated in a multi-threaded application. /// public IReadOnlyList Devices { get; } /// /// Gets a readonly list containing all registered . /// This collection should be locked when enumerated in a multi-threaded application. /// public IReadOnlyList UpdateTriggers { get; } /// /// Gets a copy of the representing this . /// public Rectangle Boundary { get; private set; } = new(new Point(0, 0), new Size(0, 0)); /// /// Gets a list of all on this . /// public IEnumerable Leds { get { lock (Devices) return _devices.SelectMany(x => x); } } #endregion #region EventHandler /// /// Represents the event-handler of the -event. /// /// The arguments provided by the event. public delegate void ExceptionEventHandler(ExceptionEventArgs args); /// /// Represents the event-handler of the -event. /// /// The arguments provided by the event. public delegate void UpdatingEventHandler(UpdatingEventArgs args); /// /// Represents the event-handler of the -event. /// /// The arguments provided by the event. public delegate void UpdatedEventHandler(UpdatedEventArgs args); /// /// Represents the event-handler of the -event. /// /// The arguments provided by the event. public delegate void SurfaceLayoutChangedEventHandler(SurfaceLayoutChangedEventArgs args); #endregion #region Events // ReSharper disable EventNeverSubscribedTo.Global /// /// Occurs when a catched exception is thrown inside the . /// public event ExceptionEventHandler? Exception; /// /// Occurs when the starts updating. /// public event UpdatingEventHandler? Updating; /// /// Occurs when the update is done. /// public event UpdatedEventHandler? Updated; /// /// Occurs when the layout of this changed. /// public event SurfaceLayoutChangedEventHandler? SurfaceLayoutChanged; // ReSharper restore EventNeverSubscribedTo.Global #endregion #region Constructors /// /// Initializes a new instance of the class. /// public RGBSurface() { _deltaTimeCounter = Stopwatch.StartNew(); Devices = new ReadOnlyCollection(_devices); UpdateTriggers = new ReadOnlyCollection(_updateTriggers); } #endregion #region Methods /// /// Perform a full update for all devices. Updates only dirty by default, or all , if flushLeds is set to true. /// /// Specifies whether all , (including clean ones) should be updated. //public void Update(bool flushLeds = false) => Update(null, new CustomUpdateData((CustomUpdateDataIndex.FLUSH_LEDS, flushLeds))); public void Update(bool flushLeds = false) => Update(null, flushLeds ? DefaultCustomUpdateData.FLUSH : DefaultCustomUpdateData.NO_FLUSH); private void Update(object? updateTrigger, ICustomUpdateData customData) => Update(updateTrigger as IUpdateTrigger, customData); private void Update(IUpdateTrigger? updateTrigger, ICustomUpdateData customData) { try { bool flushLeds = customData[CustomUpdateDataIndex.FLUSH_LEDS] as bool? ?? false; bool render = customData[CustomUpdateDataIndex.RENDER] as bool? ?? true; bool updateDevices = customData[CustomUpdateDataIndex.UPDATE_DEVICES] as bool? ?? true; lock (UpdateTriggers) lock (Devices) { OnUpdating(updateTrigger, customData); // ReSharper disable ForCanBeConvertedToForeach - 'for' has a performance benefit (no enumerator allocation) here and since 'Update' is considered a hot path it's optimized if (render) lock (_ledGroups) { // Render brushes for (int i = 0; i < _ledGroups.Count; i++) { try { Render(_ledGroups[i]); } catch (Exception ex) { OnException(ex); } } } if (updateDevices) for (int i = 0; i < _devices.Count; i++) { try { _devices[i].Update(flushLeds); } catch (Exception ex) { OnException(ex); } } // ReSharper restore ForCanBeConvertedToForeach OnUpdated(); } } catch (Exception ex) { OnException(ex); } } /// public void Dispose() { List devices; lock (Devices) devices = [.._devices]; foreach (IRGBDevice device in devices) try { Detach(device); } catch { /* We do what we can */} foreach (IUpdateTrigger updateTrigger in _updateTriggers) try { updateTrigger.Dispose(); } catch { /* We do what we can */} _ledGroups.Clear(); } /// /// Renders a ledgroup. /// /// The led group to render. /// Thrown if the of the Brush is not valid. private void Render(ILedGroup ledGroup) { IBrush? brush = ledGroup.Brush; if ((brush == null) || !brush.IsEnabled) return; using (ledGroup.ToListUnsafe(out IList leds)) { IEnumerable<(RenderTarget renderTarget, Color color)> render; switch (brush.CalculationMode) { case RenderMode.Relative: Rectangle brushRectangle = new(leds); Point offset = new(-brushRectangle.Location.X, -brushRectangle.Location.Y); brushRectangle = brushRectangle.SetLocation(new Point(0, 0)); render = brush.Render(brushRectangle, leds.Select(led => new RenderTarget(led, led.AbsoluteBoundary.Translate(offset)))); break; case RenderMode.Absolute: render = brush.Render(Boundary, leds.Select(led => new RenderTarget(led, led.AbsoluteBoundary))); break; default: throw new ArgumentException($"The CalculationMode '{brush.CalculationMode}' is not valid."); } foreach ((RenderTarget renderTarget, Color c) in render) renderTarget.Led.Color = c; } } /// /// Attaches the specified . /// /// The to attach. /// true if the could be attached; otherwise, false. public bool Attach(ILedGroup ledGroup) { lock (_ledGroups) { if (ledGroup.Surface != null) return false; ledGroup.Surface = this; _ledGroups.Add(ledGroup); _ledGroups.Sort((group1, group2) => group1.ZIndex.CompareTo(group2.ZIndex)); ledGroup.OnAttach(); return true; } } /// /// Detaches the specified . /// /// The to detache. /// true if the could be detached; false otherwise. public bool Detach(ILedGroup ledGroup) { lock (_ledGroups) { if (!_ledGroups.Remove(ledGroup)) return false; ledGroup.OnDetach(); ledGroup.Surface = null; return true; } } /// /// Attaches the specified . /// /// The to attach. public void Attach(IRGBDevice device) { lock (Devices) { if (string.IsNullOrWhiteSpace(device.DeviceInfo.DeviceName)) throw new RGBDeviceException($"The device '{device.DeviceInfo.Manufacturer} {device.DeviceInfo.Model}' has no valid name."); if (device.Surface != null) throw new RGBSurfaceException($"The device '{device.DeviceInfo.DeviceName}' is already attached to a surface."); device.Surface = this; device.BoundaryChanged += DeviceOnBoundaryChanged; _devices.Add(device); OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs.FromAddedDevice(device)); } } /// /// Detaches the specified . /// /// The to detache. /// true if the could be detached; false otherwise. public void Detach(IRGBDevice device) { lock (Devices) { if (!_devices.Contains(device)) throw new RGBSurfaceException($"The device '{device.DeviceInfo.DeviceName}' is not attached to this surface."); device.BoundaryChanged -= DeviceOnBoundaryChanged; device.Surface = null; _devices.Remove(device); OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs.FromRemovedDevice(device)); } } // ReSharper restore UnusedMember.Global private void DeviceOnBoundaryChanged(object? sender, EventArgs args) => OnSurfaceLayoutChanged((sender is IRGBDevice device) ? SurfaceLayoutChangedEventArgs.FromChangedDevice(device) : SurfaceLayoutChangedEventArgs.Misc()); private void OnSurfaceLayoutChanged(SurfaceLayoutChangedEventArgs args) { UpdateSurfaceRectangle(); SurfaceLayoutChanged?.Invoke(args); } private void UpdateSurfaceRectangle() { lock (Devices) { Rectangle devicesRectangle = new(_devices.Select(d => d.Boundary)); Boundary = Boundary.SetSize(new Size(devicesRectangle.Location.X + devicesRectangle.Size.Width, devicesRectangle.Location.Y + devicesRectangle.Size.Height)); } } /// /// Registers the provided . /// /// The to register. public void RegisterUpdateTrigger(IUpdateTrigger updateTrigger) { if (!_updateTriggers.Contains(updateTrigger)) { _updateTriggers.Add(updateTrigger); updateTrigger.Update += Update; } } /// /// Unregisters the provided . /// /// The to unregister. public void UnregisterUpdateTrigger(IUpdateTrigger updateTrigger) { if (_updateTriggers.Remove(updateTrigger)) updateTrigger.Update -= Update; } /// /// Handles the needed event-calls for an exception. /// /// The exception previously thrown. private void OnException(Exception ex) { try { Exception?.Invoke(new ExceptionEventArgs(ex)); } catch { /* Well ... that's not my fault */ } } /// /// Handles the needed event-calls before updating. /// private void OnUpdating(IUpdateTrigger? trigger, ICustomUpdateData customData) { try { double deltaTime = _deltaTimeCounter.Elapsed.TotalSeconds; _deltaTimeCounter.Restart(); Updating?.Invoke(new UpdatingEventArgs(deltaTime, trigger, customData)); } catch { /* Well ... that's not my fault */ } } /// /// Handles the needed event-calls after an update. /// private void OnUpdated() { try { Updated?.Invoke(new UpdatedEventArgs()); } catch { /* Well ... that's not my fault */ } } #endregion }