feat(overlay): flat themed buttons + full theme support for slice flags

Share one button paint routine: extract TFlatButton.Paint into
PaintFlatButton(canvas, rect, colors) and call it from both TFlatButton
and VfoOverlay.DrawButton — flag buttons now render identically to the
left panel (flat fill + border, no gloss), using theme Btn* colors.

Theme the whole flag card: the structural CLR_* palette (card bg,
border, text, dim, accents, freq, meter bg) is now derived from the
active TAppTheme via ApplyThemeColors, so flags follow light/dark like
the rest of the UI. Status accents (TX/SPLIT) stay fixed on purpose and
use a fixed dark text over their bright badges. Theme is pushed to
flags from ApplyDarkTheme and on creation via WireOverlayEvents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 14:04:21 +03:00
co-authored by Claude Opus 4.8
parent 3049f919a3
commit a3ac459abc
3 changed files with 98 additions and 90 deletions
+55 -68
View File
@@ -17,7 +17,7 @@ unit VfoOverlay;
interface
uses
Classes, SysUtils, Controls, Graphics, Types, Math;
Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme;
const
VFO_OVERLAY_ALPHA = 210;
@@ -88,6 +88,12 @@ type
FOnTxSelect: TNotifyEvent;
FOnInvalidate: TNotifyEvent;
FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.)
// Структурные цвета карточки, выведенные из FTheme (ApplyThemeColors).
CLR_CARD_BG, CLR_BORDER, CLR_SEP, CLR_TEXT, CLR_DIM,
CLR_ACCENT, CLR_ACCENT_D, CLR_BTN_NORM, CLR_BTN_HOT,
CLR_FREQ, CLR_SVAL, CLR_BAR_BG: TColor;
FSliceId: Integer; // логический id слайса (владелец — MainForm/контроллер)
FIsMain: Boolean; // главный приёмник (слайс A) — без кнопки закрытия
FOnSliceSelect: TNotifyEvent; // клик по бейджу буквы — сделать слайс активным
@@ -116,6 +122,7 @@ type
procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer);
procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
procedure RegisterHit(const HR: TRect; HV: Integer);
procedure ApplyThemeColors; // раскладывает FTheme → структурные CLR_*
function FormatFreq(Hz: Double): string;
function GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
function GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
@@ -157,6 +164,8 @@ type
const CurName: string);
procedure UpdateSMeter(DB: Double);
procedure UpdateVfo(AVfoHz: Double);
// Тема приложения (цвета кнопок берутся отсюда — как левая панель).
procedure SetTheme(const T: TAppTheme);
// Привязка к слайсу: id, буква флага, признак главного (слайс A).
procedure SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
property SliceId: Integer read FSliceId;
@@ -237,22 +246,14 @@ const
AUD_MAX_VIS = 6; // максимум видимых устройств (дальше — прокрутка)
OVL_BASE_H = PAD + HDR_H + MET_H + FRQ_H + BAR_H + PAD; // = 96
// Палитра
CLR_CARD_BG = TColor($00191919);
CLR_BORDER = TColor($00484848);
CLR_SEP = TColor($00303030);
CLR_TEXT = TColor($00E4E4E4);
CLR_DIM = TColor($00707070);
CLR_ACCENT = TColor($0040FF80); // зелёный акцент
CLR_ACCENT_D = TColor($00103018); // тёмный акцент (фон активной)
CLR_BTN_NORM = TColor($00262626);
CLR_BTN_HOT = TColor($00383838);
CLR_FREQ = TColor($0050FF80);
CLR_SVAL = TColor($0030C8FF);
CLR_BAR_BG = TColor($00101010);
CLR_TX_ON = TColor($000030E0); // красный TX идёт (BGR)
CLR_TX_SEL = TColor($0010A8F0); // янтарный: слайс выбран для передачи
CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT
// Палитра. Структурные цвета (фон/текст/акценты) берутся из темы —
// см. поля CLR_* класса + ApplyThemeColors. Здесь — только статус-акценты,
// которые намеренно одинаковы в обеих темах (яркие бейджи-статусы), и
// тёмный текст поверх них.
CLR_TX_ON = TColor($000030E0); // красный TX идёт (BGR)
CLR_TX_SEL = TColor($0010A8F0); // янтарный: слайс выбран для передачи
CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT
CLR_BADGE_TXT = TColor($00181818); // тёмный текст поверх ярких бейджей
OVL_MODE_COUNT = 8;
OVL_MODE_NAMES: array[0..7] of string = (
@@ -407,11 +408,38 @@ begin
FCacheDirty := True;
FMeterDirty := False;
FLastSTextRect := Rect(0, 0, 0, 0);
FTheme := DarkTheme; // по умолчанию тёмная; MainForm выставит актуальную
ApplyThemeColors;
Width := OVL_W;
Height := OVL_BASE_H;
Cursor := crHandPoint;
end;
// Раскладывает тему в структурные цвета карточки. Статус-акценты (TX/SPLIT)
// намеренно НЕ тематизируются — они одинаковы в обеих темах.
procedure TVfoOverlay.ApplyThemeColors;
begin
CLR_CARD_BG := FTheme.Panel;
CLR_BORDER := FTheme.Border;
CLR_SEP := FTheme.Border;
CLR_TEXT := FTheme.Text;
CLR_DIM := FTheme.TextDim;
CLR_ACCENT := FTheme.BtnTextActive; // зелёный акцент (яркий/тёмный по теме)
CLR_ACCENT_D := FTheme.BtnActive; // фон активной строки
CLR_BTN_NORM := FTheme.BtnNorm;
CLR_BTN_HOT := FTheme.BtnHot;
CLR_FREQ := FTheme.FreqActive;
CLR_SVAL := FTheme.Amber; // числовое значение S-метра
CLR_BAR_BG := FTheme.BG; // фон полоски S-метра
end;
procedure TVfoOverlay.SetTheme(const T: TAppTheme);
begin
FTheme := T;
ApplyThemeColors;
RequestInvalidate; // перерисовать хром флага с новыми цветами
end;
destructor TVfoOverlay.Destroy;
begin
FDevNames.Free;
@@ -522,61 +550,20 @@ end;
{ ---- Примитивы отрисовки ---- }
// Масштаб яркости цвета (для стеклянного градиента/фаски).
function GlossColor(Base: TColor; F: Double): TColor;
var
R, G, B: Byte;
begin
RedGreenBlue(ColorToRGB(Base), R, G, B);
Result := RGBToColor(EnsureRange(Round(R * F), 0, 255),
EnsureRange(Round(G * F), 0, 255),
EnsureRange(Round(B * F), 0, 255));
end;
procedure TVfoOverlay.DrawButton(C: TCanvas; const R: TRect; const Lbl: string;
Active, Hot: Boolean; FontSz: Integer);
var
Base: TColor;
Y, H: Integer;
RowT, Gloss: Double;
Brd: TColor;
begin
if Active then Base := CLR_ACCENT
else if Hot then Base := CLR_BTN_HOT
else Base := CLR_BTN_NORM;
// Скруглённая тёмная база = форма + рамка-фаска.
C.Brush.Color := GlossColor(Base, 0.58);
C.Brush.Style := bsSolid;
C.Pen.Color := GlossColor(Base, 0.42);
C.Pen.Style := psSolid;
C.RoundRect(R.Left, R.Top, R.Right, R.Bottom, 5, 5);
// Стеклянный вертикальный градиент: ярче вверху, темнее внизу (как ползунок зума).
H := R.Bottom - R.Top;
C.Pen.Style := psClear;
for Y := R.Top + 1 to R.Bottom - 2 do
begin
if H > 3 then RowT := (Y - (R.Top + 1)) / (H - 3) else RowT := 0.0;
Gloss := 1.30 - 0.60 * RowT;
C.Brush.Color := GlossColor(Base, Gloss);
C.FillRect(Rect(R.Left + 2, Y, R.Right - 2, Y + 1));
end;
// Блик сверху + тень снизу (объём).
C.Pen.Style := psSolid;
C.Pen.Color := GlossColor(Base, 1.55);
C.Line(R.Left + 2, R.Top + 1, R.Right - 2, R.Top + 1);
C.Pen.Color := GlossColor(Base, 0.40);
C.Line(R.Left + 2, R.Bottom - 2, R.Right - 2, R.Bottom - 2);
if Active then C.Font.Color := CLR_CARD_BG
else C.Font.Color := CLR_TEXT;
// Цвета берём из темы (как StyleButton на левой панели): рамка — по состоянию.
if Active then Brd := FTheme.BtnBorderActive else Brd := FTheme.BtnBorderNorm;
C.Font.Name := 'Courier New';
C.Font.Size := FontSz;
C.Font.Style := [];
C.Brush.Style := bsClear;
C.TextOut(R.Left + (R.Right - R.Left - C.TextWidth(Lbl)) div 2,
R.Top + (R.Bottom - R.Top - C.TextHeight('A')) div 2, Lbl);
// Общая отрисовка (FlatButton) — вид как у кнопок по всей программе.
PaintFlatButton(C, R, Lbl, Active, Hot,
FTheme.BtnNorm, FTheme.BtnActive, FTheme.BtnHot, Brd,
FTheme.BtnText, FTheme.BtnTextActive);
end;
procedure TVfoOverlay.DrawIndicator(C: TCanvas; X, Y: Integer; const Lbl: string;
@@ -917,7 +904,7 @@ begin
CurY := PAD;
{ --- Шапка: [A] BW ... [×] SPLIT [TX] --- }
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_CARD_BG);
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_BADGE_TXT);
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
if not FIsMain then
@@ -936,12 +923,12 @@ begin
// передача идёт → красный; слайс выбран для TX → янтарный; иначе → нейтральный.
R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H);
if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite)
else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_CARD_BG)
else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_BADGE_TXT)
else DrawBadge(C, R, 'TX', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_TX);
// SPLIT (левее TX) — кликабельно
R := Rect(W - PAD - 26 - 44, CurY, W - PAD - 26 - 4, CurY + HDR_BADGE_H);
if FSplit then DrawBadge(C, R, 'SPLIT', CLR_SPLIT_ON, CLR_CARD_BG)
if FSplit then DrawBadge(C, R, 'SPLIT', CLR_SPLIT_ON, CLR_BADGE_TXT)
else DrawBadge(C, R, 'SPLIT', CLR_BTN_NORM, CLR_DIM);
RegisterHit(R, HIT_SPLIT);
Inc(CurY, HDR_H);