{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

unit SMeterView;

{
  SMeterView.pas — рендеринг S-метра, TX-метра и индикаторов мощности/SWR.
  TSMeterView — изолированный класс без зависимостей на SpectrumView.
}

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

uses
  Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
  IntfGraphics, FPImage, LCLType, GraphType, AppTheme, PlatformUtils;

const
  SM_CLR_METER_ON = TColor($0000CC44);
  SM_CLR_AMBER    = TColor($0000AAFF);

type
  TSMeterView = class
  private
    FSmBitmap:      TBitmap;
    FTheme:         TAppTheme;
    FLastSMeter:    Double;
    FSMeterPeak:    Double;
    FSMeterMin:     Double;
    FLastFwdW:      Double;
    FLastSWR:       Double;
    FPAMaxPower:    Double;
    // Есть ли у железа мост детектора мощности (Caps.HasPA). У AD936x его нет:
    // FLastFwdW/FLastSWR так и стоят на значениях инициализации, и «0W / SWR
    // 1.0» на передаче — не показание, а выдумка. Шкала в этом случае рисуется
    // пустой: деления есть, заливки и цифр нет.
    FHasPATelemetry: Boolean;
    FTransmitting:  Boolean;
    FPbSMeterRight: TPaintBox;
    // Масштаб канвы: 2 на Retina, 1 на Windows/Linux. Битмап растёт во столько
    // же раз, пиксельные константы и шрифты умножаются на него.
    FScale:         Integer;

    procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect;
                             Value, Peak, MinVal: Double;
                             out ZX1, ZX2, ZY1, ZY2: Integer);
    procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect;
                             X1, X2, Y1, Y2: Integer);
    procedure DrawTXMeter(ACanvas: TCanvas; R: TRect);
  public
    constructor Create;
    destructor  Destroy; override;

    property LastSMeter:    Double  read FLastSMeter   write FLastSMeter;
    property SMeterPeak:    Double  read FSMeterPeak   write FSMeterPeak;
    property SMeterMin:     Double  read FSMeterMin    write FSMeterMin;
    property LastFwdW:      Double  read FLastFwdW     write FLastFwdW;
    property LastSWR:       Double  read FLastSWR      write FLastSWR;
    property PAMaxPower:    Double  read FPAMaxPower   write FPAMaxPower;
    property HasPATelemetry: Boolean read FHasPATelemetry write FHasPATelemetry;
    property Transmitting:  Boolean read FTransmitting write FTransmitting;
    property PbSMeterRight: TPaintBox write FPbSMeterRight;

    procedure PaintSMeterRight(Sender: TObject);
    procedure SetTheme(const T: TAppTheme);
  end;

implementation

// ════════════════════════════════════════════════════════════════════════════
// TSMeterView
// ════════════════════════════════════════════════════════════════════════════

constructor TSMeterView.Create;
begin
  inherited Create;
  FSmBitmap    := nil;
  FTheme       := DarkTheme;
  FLastSMeter  := -130;
  FSMeterPeak  := -130;
  FSMeterMin   := -130;
  FLastFwdW    := 0;
  FLastSWR     := 1;
  FPAMaxPower  := 100.0;
  FHasPATelemetry := True;
  FTransmitting := False;
  FScale       := 1;
end;

destructor TSMeterView.Destroy;
begin
  FreeAndNil(FSmBitmap);
  inherited;
end;

procedure TSMeterView.SetTheme(const T: TAppTheme);
begin
  FTheme := T;
end;

// ────────────────────────────────────────────────────────────────────────────
// DrawSMeterWide
// ────────────────────────────────────────────────────────────────────────────

procedure TSMeterView.DrawSMeterWide(ACanvas: TCanvas; R: TRect;
  Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer);
const
  DB_MIN = -127.0; DB_MAX = -13.0; DB_S9 = -73.0; DB_OVR = -43.0;
  DBM_MARKS:  array[0..5] of Double = (-120,-100,-80,-60,-40,-20);
  DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20');
  S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
  S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60');
  LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
  TICK_LONG = 5; TICK_MED = 3;
var
  CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
  CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
  CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor;
  CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
  W, H, BX, BW, LeftInfo: Integer;
  Y_BAR_TOP, Y_BAR_BOT: Integer;
  BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer;
  i, X, TW: Integer; Lbl: string; SNum, Over: Integer;
  LblDbm, LblS: string;
  TWDbm, LblZoneL, TH10, TH6, MaxTWDbm, MaxTWS, LabelNeed: Integer;
  Sc: Integer;   // масштаб канвы, см. FScale

  function DBtoX(dB: Double): Integer; inline;
  begin
    Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW);
    if Result < BX      then Result := BX;
    if Result > BX + BW then Result := BX + BW;
  end;

  procedure VLine(X2, Y1, Y2: Integer; C: TColor);
  begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;

  procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
  begin
    ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
    ACanvas.Pen.Style := psClear;
    if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
    ACanvas.Pen.Style := psSolid;
  end;

begin
  CLR_SMETER_BG    := FTheme.SMeterBG;
  CLR_BAR_GREEN    := FTheme.SMeterBarGreen;
  CLR_BAR_OVER     := FTheme.SMeterBarOver;
  CLR_PEAK_MARKER  := FTheme.SMeterPeakMarker;
  CLR_TICK_GREEN   := FTheme.SMeterTickGreen;
  CLR_TICK_WHITE   := FTheme.SMeterTickWhite;
  CLR_TICK_BLUE    := FTheme.SMeterTickBlue;
  CLR_LABEL_DBM    := FTheme.SMeterLabelDbm;
  CLR_LABEL_S      := FTheme.SMeterLabelS;
  CLR_SCALE_DBM    := FTheme.SMeterScaleDbm;
  CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen;
  CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue;
  CLR_BDR          := FTheme.SMeterBdr;

  Sc := FScale;
  W := R.Right - R.Left; H := R.Bottom - R.Top;
  ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
  if (W < 80 * Sc) or (H < 16 * Sc) then Exit;

  LblDbm := Format('%.1f', [Value]);
  if Value >= -73.0 then
  begin
    Over := Round(Value - (-73.0));
    if Over < 5 then LblS := 'S9'
    else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; LblS := Format('S9+%d', [Over]); end;
  end else if Value <= -121.0 then LblS := 'S1'
  else begin
    LblS := 'S1';
    for SNum := 1 to 8 do
      if Value >= (-121.0 + (SNum - 1) * 6.0) then LblS := 'S' + IntToStr(SNum);
  end;

  ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
  ACanvas.Font.Size := 10 * Sc; TWDbm := ACanvas.TextWidth(LblDbm);
                            MaxTWDbm := ACanvas.TextWidth('-120.0');
  MaxTWS := ACanvas.TextWidth('S9+60');
  ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [];
  LabelNeed := Max(MaxTWDbm + 2 * Sc + ACanvas.TextWidth('dBm '), MaxTWS) + LABEL_PAD * 2 * Sc;
  LeftInfo := Max(LEFT_INFO_MIN * Sc, LabelNeed + 4 * Sc);
  BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD * Sc;
  if BW < 10 * Sc then Exit;
  LblZoneL := BX - LabelNeed;

  Y_BAR_TOP := R.Top + H * 44 div 100;
  Y_BAR_BOT := R.Top + H * 58 div 100;
  S9X  := DBtoX(DB_S9); OvrX := DBtoX(DB_OVR);

  ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
  ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
  // Перо в физических пикселях: иначе на Retina все риски станут вдвое тоньше.
  ACanvas.Pen.Width := Sc;
  FillBar(BX,   Y_BAR_TOP, S9X,     Y_BAR_BOT, CLR_BAR_GREEN);
  FillBar(S9X,  Y_BAR_TOP, OvrX,    Y_BAR_BOT, CLR_BAR_GREEN);
  FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER);

  BarEnd    := DBtoX(Value);
  PeakLeft  := DBtoX(MinVal);
  PeakRight := DBtoX(Peak);
  ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight));
  ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight));
  ZY1 := Y_BAR_TOP + Sc; ZY2 := Y_BAR_BOT - Sc;

  if BarEnd > BX then
  begin
    if BarEnd <= S9X then
      FillBar(BX, Y_BAR_TOP+Sc, BarEnd, Y_BAR_BOT-Sc, CLR_BAR_GREEN)
    else begin
      FillBar(BX,  Y_BAR_TOP+Sc, S9X,    Y_BAR_BOT-Sc, CLR_BAR_GREEN);
      FillBar(S9X, Y_BAR_TOP+Sc, BarEnd, Y_BAR_BOT-Sc, CLR_BAR_OVER);
    end;
  end;
  if (PeakRight > BX) and (PeakRight <= BX + BW) then
  begin
    ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2 * Sc;
    ACanvas.MoveTo(PeakRight, Y_BAR_TOP - (Y_BAR_BOT - Y_BAR_TOP));
    ACanvas.LineTo(PeakRight, Y_BAR_BOT + (Y_BAR_BOT - Y_BAR_TOP));
    ACanvas.Pen.Width := Sc;
  end;

  ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
  ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
  ACanvas.Pen.Color := FTheme.SMeterDivider;
  ACanvas.MoveTo(BX, Y_BAR_TOP + Sc); ACanvas.LineTo(BX, Y_BAR_BOT - Sc);

  ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;
  for i := 0 to High(DBM_MARKS) do
  begin
    X := DBtoX(DBM_MARKS[i]);
    VLine(X, Y_BAR_TOP - TICK_LONG * Sc, Y_BAR_TOP - Sc, CLR_TICK_WHITE);
    Lbl := DBM_LABELS[i]; TW := ACanvas.TextWidth(Lbl);
    ACanvas.Font.Color := CLR_SCALE_DBM;
    ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG * Sc - ACanvas.TextHeight(Lbl) - Sc, Lbl);
  end;
  i := -125;
  while i < -13 do
  begin
    X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED * Sc, Y_BAR_TOP - Sc, CLR_TICK_GREEN);
    Inc(i, 5);
  end;
  for i := 0 to High(S_MARKS_DBM) do
  begin
    X := DBtoX(S_MARKS_DBM[i]);
    if S_MARKS_DBM[i] >= DB_S9 then VLine(X, Y_BAR_BOT+Sc, Y_BAR_BOT+TICK_LONG*Sc, CLR_TICK_BLUE)
    else                             VLine(X, Y_BAR_BOT+Sc, Y_BAR_BOT+TICK_LONG*Sc, CLR_TICK_GREEN);
    Lbl := S_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
    if S_MARKS_DBM[i] >= DB_S9 then ACanvas.Font.Color := CLR_SCALE_S_BLUE
    else                             ACanvas.Font.Color := CLR_SCALE_S_GREEN;
    ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG * Sc + Sc, Lbl);
  end;

  ACanvas.Brush.Style := bsClear;
  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
  ACanvas.Font.Size := 6 * Sc;  ACanvas.Font.Style := [];        TH6  := ACanvas.TextHeight('0');
  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
  ACanvas.TextOut(LblZoneL + 4 * Sc, Y_BAR_TOP - TH10 - 2 * Sc, LblDbm);
  ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE;
  ACanvas.TextOut(LblZoneL + 4 * Sc + TWDbm + 2 * Sc, Y_BAR_TOP - TH6 - 4 * Sc, 'dBm');
  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S;
  ACanvas.TextOut(LblZoneL + 4 * Sc, Y_BAR_BOT + 2 * Sc, LblS);
end;

// ────────────────────────────────────────────────────────────────────────────
// DrawSMeterZone
// ────────────────────────────────────────────────────────────────────────────

procedure TSMeterView.DrawSMeterZone(ACanvas: TCanvas; R: TRect;
                                     X1, X2, Y1, Y2: Integer);
const ALPHA = 16384;
var Img: TLazIntfImage; FC: TFPColor; IX, IY: Integer;
begin
  if (X2 <= X1) or (FSmBitmap = nil) then Exit;
  Img := FSmBitmap.CreateIntfImage;
  try
    for IY := Y1 to Y2 - 1 do
      for IX := X1 to X2 - 1 do
      begin
        FC := Img.Colors[IX, IY];
        FC.Red   := Min(65535, FC.Red   + ALPHA);
        FC.Green := Min(65535, FC.Green + ALPHA);
        FC.Blue  := Min(65535, FC.Blue  + ALPHA);
        Img.Colors[IX, IY] := FC;
      end;
    FSmBitmap.LoadFromIntfImage(Img);
  finally Img.Free; end;
end;

// ────────────────────────────────────────────────────────────────────────────
// DrawTXMeter
// ────────────────────────────────────────────────────────────────────────────

procedure TSMeterView.DrawTXMeter(ACanvas: TCanvas; R: TRect);
const
  SWR_HIGH_THRESH = 2.5;
  SWR_MAX  = 10.0;
  LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
  TICK_LONG = 5; TICK_MED = 3;
  PWR_MARKS:     array[0..4] of Double = (0.0, 0.25, 0.5, 0.75, 1.0);
  SWR_MARKS_VAL: array[0..3] of Double = (1.0, 2.5, 5.0, 10.0);
  SWR_MARKS_LBL: array[0..3] of string = ('1', '2.5', '5', '10');
var
  CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
  CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
  CLR_LABEL_DBM, CLR_LABEL_S: TColor;
  CLR_SCALE_DBM, CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
  CLR_SWR_HIGH: TColor;
  W, H, BX, BW, LeftInfo: Integer;
  Y_BAR_TOP, Y_BAR_BOT, Y_BAR_MID: Integer;
  PY1, PY2, SY1, SY2: Integer;
  PBarEnd, SBarEnd, S_WarnX: Integer;
  LblPwr, LblSWR: string;
  TWPwr, LblZoneL, TH10, TH6, MaxTWPwr, MaxTWSWR, LabelNeed: Integer;
  Sc: Integer;   // масштаб канвы, см. FScale
  i, X, TW: Integer;
  Lbl: string;
  SWRHigh: Boolean;
  PwrPct: Double;

  function PwrToX(Pct: Double): Integer; inline;
  begin
    Result := BX + Round(Max(0.0, Min(1.0, Pct)) * BW);
    if Result < BX      then Result := BX;
    if Result > BX + BW then Result := BX + BW;
  end;

  function SWRToX(SWR: Double): Integer; inline;
  begin
    Result := BX + Round(Max(0.0, Min(1.0, (SWR - 1.0) / (SWR_MAX - 1.0))) * BW);
    if Result < BX      then Result := BX;
    if Result > BX + BW then Result := BX + BW;
  end;

  procedure VLine(X2, Y1, Y2: Integer; C: TColor);
  begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end;

  procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor);
  begin
    ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid;
    ACanvas.Pen.Style := psClear;
    if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2));
    ACanvas.Pen.Style := psSolid;
  end;

begin
  CLR_SMETER_BG    := FTheme.SMeterBG;
  CLR_BAR_GREEN    := FTheme.SMeterBarGreen;
  CLR_BAR_OVER     := FTheme.SMeterBarOver;
  CLR_TICK_GREEN   := FTheme.SMeterTickGreen;
  CLR_TICK_WHITE   := FTheme.SMeterTickWhite;
  CLR_TICK_BLUE    := FTheme.SMeterTickBlue;
  CLR_LABEL_DBM    := FTheme.SMeterLabelDbm;
  CLR_LABEL_S      := FTheme.SMeterLabelS;
  CLR_SCALE_DBM    := FTheme.SMeterScaleDbm;
  CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen;
  CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue;
  CLR_BDR          := FTheme.SMeterBdr;
  CLR_SWR_HIGH     := TColor($000000CC);

  Sc := FScale;
  W := R.Right - R.Left; H := R.Bottom - R.Top;
  if (W < 80 * Sc) or (H < 16 * Sc) then Exit;

  SWRHigh := FHasPATelemetry and (FLastSWR > SWR_HIGH_THRESH);
  if FHasPATelemetry then
  begin
    LblPwr := Format('%.0fW', [FLastFwdW]);
    LblSWR := Format('SWR:%.1f', [FLastSWR]);
  end
  else
  begin
    LblPwr := '';   // измерять нечем — показания не выдумываем
    LblSWR := '';
  end;

  ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Style := [fsBold];
  ACanvas.Font.Size := 10 * Sc;
  TWPwr    := ACanvas.TextWidth(LblPwr);
  MaxTWPwr := ACanvas.TextWidth(Format('%.0fW', [FPAMaxPower]));
  MaxTWSWR := ACanvas.TextWidth('SWR:10.0');
  ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [];
  LabelNeed := Max(MaxTWPwr, MaxTWSWR) + LABEL_PAD * 2 * Sc;
  LeftInfo  := Max(LEFT_INFO_MIN * Sc, LabelNeed + 4 * Sc);
  BX := R.Left + LeftInfo; BW := W - LeftInfo - RIGHT_PAD * Sc;
  if BW < 10 * Sc then Exit;
  LblZoneL := BX - LabelNeed;

  Y_BAR_TOP := R.Top + H * 44 div 100;
  Y_BAR_BOT := R.Top + H * 58 div 100;
  Y_BAR_MID := (Y_BAR_TOP + Y_BAR_BOT) div 2;

  PY1 := Y_BAR_TOP;
  PY2 := Y_BAR_MID - Sc;
  SY1 := Y_BAR_MID + Sc;
  SY2 := Y_BAR_BOT;

  S_WarnX := SWRToX(SWR_HIGH_THRESH);

  ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
  ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
  // Перо в физических пикселях, как и в DrawSMeterWide.
  ACanvas.Pen.Width := Sc;

  FillBar(BX,      PY1, BX + BW, PY2, CLR_BAR_GREEN);
  FillBar(BX,      SY1, S_WarnX, SY2, CLR_BAR_GREEN);
  FillBar(S_WarnX, SY1, BX + BW, SY2, CLR_BAR_OVER);

  if FHasPATelemetry then
  begin
    if FPAMaxPower > 0 then PwrPct := FLastFwdW / FPAMaxPower else PwrPct := 0;
    PBarEnd := PwrToX(PwrPct);
    FillBar(BX, PY1 + Sc, PBarEnd, PY2 - Sc, SM_CLR_METER_ON);
  end;

  if FHasPATelemetry then
  begin
    SBarEnd := SWRToX(FLastSWR);
    if not SWRHigh then
      FillBar(BX, SY1 + Sc, SBarEnd, SY2 - Sc, SM_CLR_AMBER)
    else
    begin
      FillBar(BX,      SY1 + Sc, Min(S_WarnX, SBarEnd), SY2 - Sc, SM_CLR_AMBER);
      if SBarEnd > S_WarnX then
        FillBar(S_WarnX, SY1 + Sc, SBarEnd, SY2 - Sc, CLR_SWR_HIGH);
    end;
  end;

  VLine(S_WarnX, SY1, SY2, FTheme.SMeterDivider);

  ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
  ACanvas.Rectangle(LblZoneL, PY1, BX + BW, PY2);
  if SWRHigh then ACanvas.Pen.Color := CLR_SWR_HIGH
  else             ACanvas.Pen.Color := CLR_BDR;
  ACanvas.Rectangle(LblZoneL, SY1, BX + BW, SY2);

  ACanvas.Pen.Color := FTheme.SMeterDivider;
  ACanvas.MoveTo(BX, PY1 + Sc); ACanvas.LineTo(BX, PY2 - Sc);
  ACanvas.MoveTo(BX, SY1 + Sc); ACanvas.LineTo(BX, SY2 - Sc);

  ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear;

  i := 5;
  while i < 100 do
  begin
    if (i mod 25) <> 0 then
    begin
      X := PwrToX(i / 100.0);
      VLine(X, PY1 - TICK_MED * Sc, PY1 - Sc, CLR_TICK_GREEN);
    end;
    Inc(i, 5);
  end;

  for i := 0 to 4 do
  begin
    X := PwrToX(PWR_MARKS[i]);
    VLine(X, PY1 - TICK_LONG * Sc, PY1 - Sc, CLR_TICK_WHITE);
    // Ватты на делениях считаются от Max power, а он есть только там, где есть
    // детектор. Без него оставляем голые деления: подписать их нечем.
    if not FHasPATelemetry then Continue;
    Lbl := Format('%.0f', [PWR_MARKS[i] * FPAMaxPower]);
    TW  := ACanvas.TextWidth(Lbl);
    ACanvas.Font.Color := CLR_SCALE_DBM;
    ACanvas.TextOut(X - TW div 2, PY1 - TICK_LONG * Sc - ACanvas.TextHeight(Lbl) - Sc, Lbl);
  end;

  for i := 2 to 9 do
  begin
    if i = 5 then Continue;
    X := SWRToX(i);
    if i >= 3 then VLine(X, SY2 + Sc, SY2 + TICK_MED * Sc, CLR_TICK_BLUE)
    else           VLine(X, SY2 + Sc, SY2 + TICK_MED * Sc, CLR_TICK_GREEN);
  end;

  for i := 0 to 3 do
  begin
    X := SWRToX(SWR_MARKS_VAL[i]);
    if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
      VLine(X, SY2 + Sc, SY2 + TICK_LONG * Sc, CLR_TICK_BLUE)
    else
      VLine(X, SY2 + Sc, SY2 + TICK_LONG * Sc, CLR_TICK_GREEN);
    Lbl := SWR_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl);
    if SWR_MARKS_VAL[i] >= SWR_HIGH_THRESH then
    begin
      if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
      else             ACanvas.Font.Color := CLR_SCALE_S_BLUE;
    end else
      ACanvas.Font.Color := CLR_SCALE_S_GREEN;
    ACanvas.TextOut(X - TW div 2, SY2 + TICK_LONG * Sc + Sc, Lbl);
  end;

  ACanvas.Brush.Style := bsClear;
  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; TH10 := ACanvas.TextHeight('0');
  ACanvas.Font.Size := 6 * Sc;  ACanvas.Font.Style := [];        TH6  := ACanvas.TextHeight('0');

  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
  ACanvas.TextOut(LblZoneL + 4 * Sc, PY1 - TH10 - 2 * Sc, LblPwr);

  ACanvas.Font.Size := 10 * Sc; ACanvas.Font.Style := [fsBold];
  if SWRHigh then ACanvas.Font.Color := CLR_SWR_HIGH
  else             ACanvas.Font.Color := CLR_LABEL_S;
  ACanvas.TextOut(LblZoneL + 4 * Sc, SY2 + 2 * Sc, LblSWR);

  if SWRHigh then
  begin
    ACanvas.Font.Size := 6 * Sc; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_SWR_HIGH;
    ACanvas.TextOut(LblZoneL + 4 * Sc + TWPwr + 2 * Sc, PY1 - TH6 - 4 * Sc, 'SWR High');
  end;
end;

// ────────────────────────────────────────────────────────────────────────────
// PaintSMeterRight
// ────────────────────────────────────────────────────────────────────────────

procedure TSMeterView.PaintSMeterRight(Sender: TObject);
var PB: TPaintBox; W, H, BmpW, BmpH, ZX1, ZX2, ZY1, ZY2: Integer;
begin
  PB := TPaintBox(Sender);
  W := PB.Width; H := PB.Height;
  if (W <= 0) or (H <= 0) then Exit;

  // Битмап держим в физических пикселях канвы, иначе Cocoa растянет его вдвое
  // и картинка замылится. На Windows/Linux FScale=1 и всё как было.
  FScale := GetControlScale(PB);
  BmpW := W * FScale; BmpH := H * FScale;

  if (FSmBitmap = nil) or (FSmBitmap.Width <> BmpW) or (FSmBitmap.Height <> BmpH) then
  begin
    FreeAndNil(FSmBitmap);
    FSmBitmap := TBitmap.Create;
    FSmBitmap.SetSize(BmpW, BmpH);
  end;
  if FTransmitting then
  begin
    DrawTXMeter(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH));
  end else
  begin
    DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH),
                   FLastSMeter, FSMeterPeak, FSMeterMin,
                   ZX1, ZX2, ZY1, ZY2);
    DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, BmpW, BmpH), ZX1, ZX2, ZY1, ZY2);
  end;

  if FScale = 1 then
    PB.Canvas.Draw(0, 0, FSmBitmap)
  else
    PB.Canvas.StretchDraw(Rect(0, 0, W, H), FSmBitmap);
end;

end.
