using System;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Rendering;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Controls;
///
/// Usage bar with three draggable stage markers: soft (throttle to 2 slots), hard (1 slot) and gate
/// (queue paused). Positions are computed against the control's real width — no hardcoded track
/// size — and the drag math lives in so it stays testable.
/// Values are written back through TwoWay bindings while dragging;
/// fires once on release, which is when the host persists them.
/// A row without thresholds (plan-dependent scoped buckets) renders as a plain read-only bar.
///
public sealed class UsageGaugeBar : Control, ICustomHitTest
{
/// How close the pointer has to be to grab a marker.
private const double GrabRadiusPx = 12;
private const double TrackHeightPx = 10;
private const double MarkerWidthPx = 2;
public static readonly StyledProperty PercentProperty =
AvaloniaProperty.Register(nameof(Percent));
public static readonly StyledProperty IsWarnProperty =
AvaloniaProperty.Register(nameof(IsWarn));
public static readonly StyledProperty SoftPctProperty =
AvaloniaProperty.Register(
nameof(SoftPct), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty HardPctProperty =
AvaloniaProperty.Register(
nameof(HardPct), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty GatePctProperty =
AvaloniaProperty.Register(
nameof(GatePct), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty TrackBrushProperty =
AvaloniaProperty.Register(nameof(TrackBrush));
public static readonly StyledProperty FillBrushProperty =
AvaloniaProperty.Register(nameof(FillBrush));
public static readonly StyledProperty WarnFillBrushProperty =
AvaloniaProperty.Register(nameof(WarnFillBrush));
public static readonly StyledProperty SoftMarkerBrushProperty =
AvaloniaProperty.Register(nameof(SoftMarkerBrush));
public static readonly StyledProperty HardMarkerBrushProperty =
AvaloniaProperty.Register(nameof(HardMarkerBrush));
public static readonly StyledProperty GateMarkerBrushProperty =
AvaloniaProperty.Register(nameof(GateMarkerBrush));
public static readonly StyledProperty CommitCommandProperty =
AvaloniaProperty.Register(nameof(CommitCommand));
static UsageGaugeBar()
{
AffectsRender(
PercentProperty, IsWarnProperty, SoftPctProperty, HardPctProperty, GatePctProperty,
TrackBrushProperty, FillBrushProperty, WarnFillBrushProperty,
SoftMarkerBrushProperty, HardMarkerBrushProperty, GateMarkerBrushProperty);
}
private UsageThresholdDrag.Stage? _dragging;
public double Percent
{
get => GetValue(PercentProperty);
set => SetValue(PercentProperty, value);
}
public bool IsWarn
{
get => GetValue(IsWarnProperty);
set => SetValue(IsWarnProperty, value);
}
public int? SoftPct
{
get => GetValue(SoftPctProperty);
set => SetValue(SoftPctProperty, value);
}
public int? HardPct
{
get => GetValue(HardPctProperty);
set => SetValue(HardPctProperty, value);
}
public int? GatePct
{
get => GetValue(GatePctProperty);
set => SetValue(GatePctProperty, value);
}
public IBrush? TrackBrush
{
get => GetValue(TrackBrushProperty);
set => SetValue(TrackBrushProperty, value);
}
public IBrush? FillBrush
{
get => GetValue(FillBrushProperty);
set => SetValue(FillBrushProperty, value);
}
public IBrush? WarnFillBrush
{
get => GetValue(WarnFillBrushProperty);
set => SetValue(WarnFillBrushProperty, value);
}
public IBrush? SoftMarkerBrush
{
get => GetValue(SoftMarkerBrushProperty);
set => SetValue(SoftMarkerBrushProperty, value);
}
public IBrush? HardMarkerBrush
{
get => GetValue(HardMarkerBrushProperty);
set => SetValue(HardMarkerBrushProperty, value);
}
public IBrush? GateMarkerBrush
{
get => GetValue(GateMarkerBrushProperty);
set => SetValue(GateMarkerBrushProperty, value);
}
public ICommand? CommitCommand
{
get => GetValue(CommitCommandProperty);
set => SetValue(CommitCommandProperty, value);
}
private bool IsAdjustable => SoftPct is not null && HardPct is not null && GatePct is not null;
// Custom hit test (point is in local coordinates): the control draws itself, so the whole
// rectangle takes the pointer — not just the pixels the track happens to cover.
public bool HitTest(Point point) => new Rect(Bounds.Size).Contains(point);
public override void Render(DrawingContext context)
{
var width = Bounds.Width;
var height = Bounds.Height;
if (width <= 0 || height <= 0) return;
var top = Math.Max(0, (height - TrackHeightPx) / 2);
var trackHeight = Math.Min(TrackHeightPx, height);
var radius = trackHeight / 2;
// Transparent full-bounds fill keeps the grab area the whole control, not just the track.
context.FillRectangle(Brushes.Transparent, new Rect(0, 0, width, height));
if (TrackBrush is { } track)
context.DrawRectangle(track, null, new RoundedRect(new Rect(0, top, width, trackHeight), radius));
var fillWidth = width * Math.Clamp(Percent, 0, 100) / 100.0;
var fill = IsWarn ? WarnFillBrush ?? FillBrush : FillBrush;
if (fillWidth > 0 && fill is not null)
context.DrawRectangle(fill, null, new RoundedRect(new Rect(0, top, fillWidth, trackHeight), radius));
DrawMarker(context, SoftPct, SoftMarkerBrush, width, height);
DrawMarker(context, HardPct, HardMarkerBrush, width, height);
DrawMarker(context, GatePct, GateMarkerBrush, width, height);
}
private static void DrawMarker(DrawingContext context, int? percent, IBrush? brush, double width, double height)
{
if (percent is not { } value || brush is null) return;
var x = Math.Clamp(width * Math.Clamp(value, 0, 100) / 100.0 - MarkerWidthPx / 2, 0, Math.Max(0, width - MarkerWidthPx));
context.FillRectangle(brush, new Rect(x, 0, MarkerWidthPx, height));
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (!IsAdjustable) return;
var percent = PercentAt(e.GetPosition(this).X);
_dragging = UsageThresholdDrag.Nearest(
SoftPct!.Value, HardPct!.Value, GatePct!.Value, percent, GrabTolerancePercent());
if (_dragging is null) return;
e.Pointer.Capture(this);
ApplyDrag(_dragging.Value, percent);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (!IsAdjustable) return;
var percent = PercentAt(e.GetPosition(this).X);
if (_dragging is { } stage)
{
ApplyDrag(stage, percent);
e.Handled = true;
return;
}
var hover = UsageThresholdDrag.Nearest(
SoftPct!.Value, HardPct!.Value, GatePct!.Value, percent, GrabTolerancePercent());
Cursor = new Cursor(hover is null ? StandardCursorType.Arrow : StandardCursorType.SizeWestEast);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (_dragging is null) return;
_dragging = null;
e.Pointer.Capture(null);
e.Handled = true;
if (CommitCommand is { } command && command.CanExecute(null))
command.Execute(null);
}
private void ApplyDrag(UsageThresholdDrag.Stage stage, double percent)
{
var (soft, hard, gate) = UsageThresholdDrag.Apply(
SoftPct!.Value, HardPct!.Value, GatePct!.Value, stage, percent);
SoftPct = soft;
HardPct = hard;
GatePct = gate;
}
private double PercentAt(double x) => Bounds.Width <= 0 ? 0 : Math.Clamp(x / Bounds.Width * 100.0, 0, 100);
private double GrabTolerancePercent() => Bounds.Width <= 0 ? 0 : GrabRadiusPx / Bounds.Width * 100.0;
}