add light/dark theme with global persistence

- AppTheme.pas: новый файл, TAppTheme record + DarkTheme()/LightTheme()
- FlatSlider: цвета через SetThemeColors вместо хардкода
- SpectrumView: все цвета из TAppTheme (SetTheme)
- SettingsForm: ApplyTheme(T) с рекурсивным обходом контролов
- DeviceForm: SetTheme(T) — перекраска всех элементов включая TEdit
- Settings: SaveTheme/LoadTheme в корневой секции preferences (не привязано к устройству)
- MainForm: тема загружается при старте, сохраняется при изменении

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 19:00:45 +03:00
co-authored by Claude Sonnet 4.6
parent f89b15730e
commit d9a2157719
8 changed files with 785 additions and 173 deletions
+51 -31
View File
@@ -1,7 +1,8 @@
unit FlatSlider;
{ Кастомный горизонтальный ползунок с тёмной темой — замена TTrackBar.
Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux. }
{ Кастомный горизонтальный ползунок — замена TTrackBar.
Полностью отрисован через Canvas, одинаково выглядит на Windows и Linux.
Цвета настраиваемые: применяй SetThemeColors(T: TAppTheme) для смены темы. }
{$mode objfpc}{$H+}
@@ -20,6 +21,14 @@ type
FDragging: Boolean;
FHot: Boolean;
FOnChange: TNotifyEvent;
// Цвета (настраиваются через SetThemeColors или напрямую)
FClrBG: TColor;
FClrTrackEmpty: TColor;
FClrTrackFill: TColor;
FClrThumbNorm: TColor;
FClrThumbHot: TColor;
FClrThumbDrag: TColor;
FClrThumbBdr: TColor;
procedure SetPosition(V: Integer);
procedure SetMin(V: Integer);
procedure SetMax(V: Integer);
@@ -39,6 +48,11 @@ type
public
constructor Create(AOwner: TComponent); override;
// Применить набор цветов из темы
procedure SetThemeColors(
ABG, ATrackEmpty, ATrackFill,
AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor);
property Min: Integer read FMin write SetMin;
property Max: Integer read FMax write SetMax;
property Position: Integer read FPosition write SetPosition;
@@ -51,17 +65,9 @@ type
implementation
const
THUMB_R = 6; // радиус ручки (px)
TRACK_H = 4; // высота рельса (px)
PAD = THUMB_R + 2; // боковой отступ чтобы ручка не вылезала
CLR_BG = TColor($00181818); // совпадает с CLR_PANEL
CLR_TRACK_EMPTY = TColor($00323232);
CLR_TRACK_FILL = TColor($00205018); // тёмно-зелёный, как тема
CLR_THUMB_NORM = TColor($00407840);
CLR_THUMB_HOT = TColor($0066BB66);
CLR_THUMB_DRAG = TColor($0099EE99);
CLR_THUMB_BDR = TColor($00122012);
THUMB_R = 6;
TRACK_H = 4;
PAD = THUMB_R + 2;
constructor TFlatSlider.Create(AOwner: TComponent);
begin
@@ -73,11 +79,32 @@ begin
FDragging := False;
FHot := False;
Cursor := crHandPoint;
// Тёмная тема по умолчанию
FClrBG := TColor($00181818);
FClrTrackEmpty := TColor($00323232);
FClrTrackFill := TColor($00205018);
FClrThumbNorm := TColor($00407840);
FClrThumbHot := TColor($0066BB66);
FClrThumbDrag := TColor($0099EE99);
FClrThumbBdr := TColor($00122012);
end;
procedure TFlatSlider.SetThemeColors(
ABG, ATrackEmpty, ATrackFill,
AThumbNorm, AThumbHot, AThumbDrag, AThumbBdr: TColor);
begin
FClrBG := ABG;
FClrTrackEmpty := ATrackEmpty;
FClrTrackFill := ATrackFill;
FClrThumbNorm := AThumbNorm;
FClrThumbHot := AThumbHot;
FClrThumbDrag := AThumbDrag;
FClrThumbBdr := AThumbBdr;
Invalidate;
end;
procedure TFlatSlider.SetPosition(V: Integer);
var
C: Integer;
var C: Integer;
begin
C := EnsureRange(V, FMin, FMax);
if FPosition = C then Exit;
@@ -102,10 +129,8 @@ begin
Invalidate;
end;
// Позиция значения → пиксельная X-координата ручки
function TFlatSlider.PosToX(APos: Integer): Integer;
var
Range, TrackW: Integer;
var Range, TrackW: Integer;
begin
Range := FMax - FMin;
TrackW := Width - PAD * 2;
@@ -116,10 +141,8 @@ begin
Result := PAD + Round((APos - FMin) / Range * TrackW);
end;
// Пиксельная X-координата → значение позиции
function TFlatSlider.XToPos(AX: Integer): Integer;
var
Range, TrackW, RawX: Integer;
var Range, TrackW, RawX: Integer;
begin
Range := FMax - FMin;
TrackW := Width - PAD * 2;
@@ -141,32 +164,29 @@ begin
CY := Height div 2;
TX := PosToX(FPosition);
Canvas.Brush.Color := CLR_BG;
Canvas.Brush.Color := FClrBG;
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Style := psClear;
Canvas.FillRect(ClientRect);
// Рельс — весь
TR := Rect(PAD, CY - TRACK_H div 2,
Width - PAD, CY + (TRACK_H + 1) div 2);
Canvas.Brush.Color := CLR_TRACK_EMPTY;
Canvas.Brush.Color := FClrTrackEmpty;
Canvas.RoundRect(TR.Left, TR.Top, TR.Right, TR.Bottom, TRACK_H, TRACK_H);
// Рельс — заполненная часть
if not FReversed then begin FL := PAD; FR := TX; end
else begin FL := TX; FR := Width - PAD; end;
if FR > FL then
begin
Canvas.Brush.Color := CLR_TRACK_FILL;
Canvas.Brush.Color := FClrTrackFill;
Canvas.RoundRect(FL, TR.Top, FR, TR.Bottom, TRACK_H, TRACK_H);
end;
// Ручка — круг
if FDragging then ThC := CLR_THUMB_DRAG
else if FHot then ThC := CLR_THUMB_HOT
else ThC := CLR_THUMB_NORM;
if FDragging then ThC := FClrThumbDrag
else if FHot then ThC := FClrThumbHot
else ThC := FClrThumbNorm;
Canvas.Brush.Color := ThC;
Canvas.Pen.Color := CLR_THUMB_BDR;
Canvas.Pen.Color := FClrThumbBdr;
Canvas.Pen.Style := psSolid;
Canvas.Pen.Width := 1;
Canvas.Ellipse(TX - THUMB_R, CY - THUMB_R,