using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using ytLive.Helpers; namespace ytLive.Controls; /// A segmented level ladder for the audio dock. /// /// Drawn in ONE pass. Twenty-four Rectangles per meter would /// mean hundreds of elements updating on the UI thread; a single drawing call does not. The /// consumer updates at ~20 Hz — music-level smoothness is not needed and the /// paint cost should never compete with the compositor. /// /// Level is a 0…1 display fraction (see ), never a gain setting: the /// ladder shows what was measured. public sealed class SegmentMeter : FrameworkElement { public static readonly DependencyProperty LevelProperty = DependencyProperty.Register( nameof(Level), typeof(double), typeof(SegmentMeter), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender)); /// Peak-hold mark, 0…1. Falls back slowly so a clip stays visible long enough to see. public static readonly DependencyProperty PeakProperty = DependencyProperty.Register( nameof(Peak), typeof(double), typeof(SegmentMeter), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty SegmentsProperty = DependencyProperty.Register( nameof(Segments), typeof(int), typeof(SegmentMeter), new FrameworkPropertyMetadata(24, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( nameof(Orientation), typeof(Orientation), typeof(SegmentMeter), new FrameworkPropertyMetadata(Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsRender)); public double Level { get => (double)GetValue(LevelProperty); set => SetValue(LevelProperty, value); } public double Peak { get => (double)GetValue(PeakProperty); set => SetValue(PeakProperty, value); } public int Segments { get => (int)GetValue(SegmentsProperty); set => SetValue(SegmentsProperty, value); } public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } public SegmentMeter() { // Token-driven, not literal: the ladder cannot drift from DESIGN.md. SetResourceReference(NominalBrushProperty, "Brush.Meter.Nominal"); SetResourceReference(CautionBrushProperty, "Brush.Meter.Caution"); SetResourceReference(PeakBrushProperty, "Brush.Meter.Peak"); SetResourceReference(DimBrushProperty, "Brush.SurfaceContainerHighest"); } private static readonly DependencyProperty NominalBrushProperty = DependencyProperty.Register( "NominalBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Cyan, FrameworkPropertyMetadataOptions.AffectsRender)); private static readonly DependencyProperty CautionBrushProperty = DependencyProperty.Register( "CautionBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Orange, FrameworkPropertyMetadataOptions.AffectsRender)); private static readonly DependencyProperty PeakBrushProperty = DependencyProperty.Register( "PeakBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.Red, FrameworkPropertyMetadataOptions.AffectsRender)); private static readonly DependencyProperty DimBrushProperty = DependencyProperty.Register( "DimBrush", typeof(Brush), typeof(SegmentMeter), new FrameworkPropertyMetadata(Brushes.DimGray, FrameworkPropertyMetadataOptions.AffectsRender)); private Brush NominalBrush => (Brush)GetValue(NominalBrushProperty); private Brush CautionBrush => (Brush)GetValue(CautionBrushProperty); private Brush PeakBrush => (Brush)GetValue(PeakBrushProperty); private Brush DimBrush => (Brush)GetValue(DimBrushProperty); private const double Gap = 2; protected override void OnRender(DrawingContext dc) { var count = Math.Max(1, Segments); var vertical = Orientation == Orientation.Vertical; var extent = vertical ? ActualHeight : ActualWidth; var thickness = vertical ? ActualWidth : ActualHeight; if (extent <= 0 || thickness <= 0) return; var segmentSize = (extent - Gap * (count - 1)) / count; if (segmentSize <= 0) return; var lit = Math.Clamp(Level, 0, 1) * count; for (var i = 0; i < count; i++) { // Segment 0 sits at the bottom (vertical) or the left (horizontal). var fraction = (i + 1) / (double)count; var brush = fraction >= MeterScale.ToFraction(MeterScale.PeakDb) ? PeakBrush : fraction >= MeterScale.ToFraction(MeterScale.CautionDb) ? CautionBrush : NominalBrush; if (i >= lit) brush = DimBrush; var offset = i * (segmentSize + Gap); var rect = vertical ? new Rect(0, extent - offset - segmentSize, thickness, segmentSize) : new Rect(offset, 0, segmentSize, thickness); dc.DrawRectangle(brush, null, rect); } // Peak-hold: a 2px marker at the held level, drawn above the ladder. var peak = Math.Clamp(Peak, 0, 1); if (peak <= 0) return; var line = vertical ? new Rect(0, extent - peak * extent - 1, thickness, 2) : new Rect(peak * extent - 1, 0, 2, thickness); dc.DrawRectangle(PeakBrush, null, line); } }