@using System.Globalization @inject IJSRuntime JS @implements IAsyncDisposable @if (_isDragging) {
}
@GetDisplayLabel()
@code { [Parameter] public double Value { get; set; } [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public double Min { get; set; } = 0; [Parameter] public double Max { get; set; } = 100; [Parameter] public double Step { get; set; } = 1; [Parameter] public string Label { get; set; } = ""; [Parameter] public int Size { get; set; } = 50; [Parameter] public MudBlazor.Color Color { get; set; } = MudBlazor.Color.Primary; [Parameter] public bool HoldValue { get; set; } = false; private ElementReference _knobRef; private IJSObjectReference? _knobModule; private bool _isDragging = false; private double _lastMouseY = 0; private double _dragValue = 0; private double GetNormalizedValue() { // Use drag value during dragging if HoldValue is enabled, otherwise use current Value double currentValue = (_isDragging && HoldValue) ? _dragValue : Value; return Math.Max(0, Math.Min(1, (currentValue - Min) / (Max - Min))); } private string GetDisplayLabel() { // Show drag value during dragging if HoldValue is enabled, otherwise use provided Label if (_isDragging && HoldValue) { return _dragValue.ToString("F0"); } return Label; } private string GetKnobColor() { return Color switch { MudBlazor.Color.Primary => "var(--mud-palette-primary)", MudBlazor.Color.Secondary => "var(--mud-palette-secondary)", MudBlazor.Color.Success => "var(--mud-palette-success)", MudBlazor.Color.Warning => "var(--mud-palette-warning)", MudBlazor.Color.Error => "var(--mud-palette-error)", MudBlazor.Color.Info => "var(--mud-palette-info)", _ => "var(--mud-palette-primary)" }; } private string GetBackgroundStrokeDashArray() { double circumference = 2 * Math.PI * 35; // radius = 35 double maxArc = circumference * 0.75; // 270 degrees return $"{maxArc} {circumference}"; } private string GetStrokeDashArray() { double circumference = 2 * Math.PI * 35; // radius = 35 double maxArc = circumference * 0.75; // 270 degrees double valueArc = maxArc * GetNormalizedValue(); return $"{valueArc} {circumference}"; } private string GetStrokeDashOffset() { // No offset needed - arc should start from the beginning and grow return "0"; } private string GetPointerX() { double angle = -225 + (270 * GetNormalizedValue()); // -225 to +45 degrees (centered on vertical) double radians = angle * Math.PI / 180; double x = 50 + (25 * Math.Cos(radians)); // radius = 25 for pointer return x.ToString("F1", CultureInfo.InvariantCulture); } private string GetPointerY() { double angle = -225 + (270 * GetNormalizedValue()); double radians = angle * Math.PI / 180; double y = 50 + (25 * Math.Sin(radians)); return y.ToString("F1", CultureInfo.InvariantCulture); } private async Task OnPointerDown(PointerEventArgs e) { _isDragging = true; _lastMouseY = e.ClientY; _dragValue = Value; // Initialize drag value with current value // Load the JS module on first drag (lazy — avoids import cost until the user interacts). _knobModule ??= await JS.InvokeAsync( "import", "./_content/DeepDrftShared.Client/js/knob/knob.js"); // Capture the pointer on the knob element so pointermove/pointerup are delivered // even when the cursor leaves the browser window mid-drag. await _knobModule.InvokeVoidAsync("capturePointer", _knobRef, e.PointerId); // The full-viewport capture div (rendered when _isDragging) is a belt-and-suspenders // guard that blocks stray clicks on underlying content while dragging. StateHasChanged(); } // Delivered to the knob element because setPointerCapture routes the captured pointer here. private async Task OnPointerMove(PointerEventArgs e) { if (_isDragging) { await UpdateValueFromPointer(e); } } // pointerup implicitly releases pointer capture — no explicit releasePointerCapture needed. private async Task OnPointerUp(PointerEventArgs e) { if (_isDragging && HoldValue) { // If HoldValue is enabled, emit the final value now if (Math.Abs(_dragValue - Value) > 0.001) { await ValueChanged.InvokeAsync(_dragValue); } } _isDragging = false; StateHasChanged(); } // Pointer capture cancelled by OS (e.g. Alt+Tab, system gesture) — end drag cleanly. // Delivered to the knob element (the capturing element). Release capture explicitly since // the implicit release on pointerup does not apply to pointercancel. private async Task OnPointerCancel(PointerEventArgs e) { if (_knobModule != null) { try { await _knobModule.InvokeVoidAsync("releasePointer", _knobRef, e.PointerId); } catch (JSException) { /* element may already be gone */ } } _isDragging = false; StateHasChanged(); } public async ValueTask DisposeAsync() { if (_knobModule != null) { try { await _knobModule.DisposeAsync(); } catch (JSDisconnectedException) { /* circuit torn down */ } } GC.SuppressFinalize(this); } private async Task UpdateValueFromPointer(PointerEventArgs e) { // Calculate vertical delta from last pointer position double deltaY = _lastMouseY - e.ClientY; // Inverted: up = positive, down = negative _lastMouseY = e.ClientY; // Sensitivity factor (pixels to move for full range) double sensitivity = 100.0; // Calculate change in normalized value based on vertical movement double normalizedDelta = deltaY / sensitivity; double currentNormalized = GetNormalizedValue(); double newNormalized = Math.Max(0, Math.Min(1, currentNormalized + normalizedDelta)); // Convert to actual value double newValue = Min + (newNormalized * (Max - Min)); // Apply step if (Step > 0) { newValue = Math.Round(newValue / Step) * Step; } if (Math.Abs(newValue - (HoldValue ? _dragValue : Value)) > 0.001) // Avoid unnecessary updates { if (HoldValue) { // Update drag value only, don't emit ValueChanged yet _dragValue = newValue; StateHasChanged(); // Update visual only } else { // Original behavior - emit ValueChanged immediately Value = newValue; await ValueChanged.InvokeAsync(Value); } } } }