// SPDX-License-Identifier: LGPL-3.0-linking-exception unit BGRATheme; {$mode objfpc}{$H+} interface uses Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, BGRABitmap, BGRABitmapTypes, BGRASVGImageList; type TBGRAThemeButtonState = (btbsNormal, btbsHover, btbsActive, btbsDisabled); { TBGRAThemeSurface } TBGRAThemeSurface = class private FBitmap: TBGRABitmap; FBitmapRect: TRect; FCanvasScale: single; FDestCanvas: TCanvas; FLclDPI: integer; function GetBitmap: TBGRABitmap; function GetBitmapDPI: integer; procedure SetBitmapRect(AValue: TRect); public constructor Create(AControl: TCustomControl); constructor Create(ADestRect: TRect; ADestCanvas: TCanvas; ACanvasScale: single; ALclDPI: integer); destructor Destroy; override; procedure DrawBitmap; procedure DiscardBitmap; procedure BitmapColorOverlay(AColor: TBGRAPixel; AOperation: TBlendOperation = boTransparent); overload; function ScaleForCanvas(AValue: integer; AFromDPI: integer = 96): integer; function ScaleForBitmap(AValue: integer; AFromDPI: integer = 96): integer; function ScaleForBitmap(const ARect: TRect; AFromDPI: integer = 96): TRect; property DestCanvas: TCanvas read FDestCanvas; property DestCanvasDPI: integer read FLclDPI; property Bitmap: TBGRABitmap read GetBitmap; property BitmapRect: TRect read FBitmapRect write SetBitmapRect; property BitmapDPI: integer read GetBitmapDPI; end; TBGRATheme = class; { TBGRAThemeControl } TBGRAThemeControl = class(TCustomControl) private FTheme: TBGRATheme; procedure SetTheme(AValue: TBGRATheme); public destructor Destroy; override; published property Theme: TBGRATheme read FTheme write SetTheme; end; { TBGRATheme } TBGRATheme = class(TComponent) private FThemedControls: TList; function GetThemedControl(AIndex: integer): TBGRAThemeControl; function GetThemedControlCount: integer; procedure AddThemedControl(AControl: TBGRAThemeControl); procedure RemoveThemedControl(AControl: TBGRAThemeControl); protected public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure InvalidateThemedControls; function PreferredButtonWidth(const hasGlyph: boolean): Integer; virtual; function PreferredButtonHeight(const hasGlyph: boolean): Integer; virtual; procedure DrawButton(Caption: string; State: TBGRAThemeButtonState; Focused: boolean; ARect: TRect; ASurface: TBGRAThemeSurface; AImageIndex: Integer = -1; AImageList: TBGRASVGImageList = nil); virtual; procedure DrawRadioButton(Caption: string; State: TBGRAThemeButtonState; {%H-}Focused: boolean; Checked: boolean; ARect: TRect; ASurface: TBGRAThemeSurface); virtual; procedure DrawCheckBox(Caption: string; State: TBGRAThemeButtonState; {%H-}Focused: boolean; Checked: boolean; ARect: TRect; ASurface: TBGRAThemeSurface); virtual; property ThemedControlCount: integer read GetThemedControlCount; property ThemedControl[AIndex: integer]: TBGRAThemeControl read GetThemedControl; published end; var BGRADefaultTheme: TBGRATheme; procedure Register; implementation uses LCLType; procedure Register; begin RegisterComponents('BGRA Themes', [TBGRATheme]); end; { TBGRAThemeControl } procedure TBGRAThemeControl.SetTheme(AValue: TBGRATheme); begin if FTheme=AValue then Exit; if Assigned(AValue) then AValue.RemoveThemedControl(self); FTheme:=AValue; if Assigned(AValue) then AValue.AddThemedControl(self); Invalidate; end; destructor TBGRAThemeControl.Destroy; begin if Assigned(FTheme) then FTheme.RemoveThemedControl(self); inherited Destroy; end; { TBGRAThemeSurface } function TBGRAThemeSurface.GetBitmap: TBGRABitmap; begin if FBitmap = nil then FBitmap := TBGRABitmap.Create(round(FBitmapRect.Width * FCanvasScale), round(FBitmapRect.Height * FCanvasScale)); result := FBitmap; end; function TBGRAThemeSurface.GetBitmapDPI: integer; begin result := round(FLclDPI*FCanvasScale); end; procedure TBGRAThemeSurface.SetBitmapRect(AValue: TRect); begin if FBitmapRect=AValue then Exit; DiscardBitmap; FBitmapRect:=AValue; end; constructor TBGRAThemeSurface.Create(AControl: TCustomControl); var parentForm: TCustomForm; lclDPI: Integer; begin parentForm := GetParentForm(AControl, False); if Assigned(parentForm) then lclDPI := parentForm.PixelsPerInch else lclDPI := Screen.PixelsPerInch; Create(AControl.ClientRect, AControl.Canvas, AControl.GetCanvasScaleFactor, lclDPI); end; constructor TBGRAThemeSurface.Create(ADestRect: TRect; ADestCanvas: TCanvas; ACanvasScale: single; ALclDPI: integer); begin FBitmap := nil; FBitmapRect := ADestRect; FDestCanvas := ADestCanvas; FCanvasScale:= ACanvasScale; FLclDPI:= ALclDPI; end; destructor TBGRAThemeSurface.Destroy; begin FBitmap.Free; inherited Destroy; end; procedure TBGRAThemeSurface.DrawBitmap; begin if FBitmap = nil then exit; FBitmap.Draw(FDestCanvas, FBitmapRect, false); end; procedure TBGRAThemeSurface.DiscardBitmap; begin FreeAndNil(FBitmap); end; procedure TBGRAThemeSurface.BitmapColorOverlay(AColor: TBGRAPixel; AOperation: TBlendOperation); begin if AColor.alpha <> 0 then Bitmap.BlendOver(AColor, AOperation, AColor.alpha, false, true); end; function TBGRAThemeSurface.ScaleForCanvas(AValue: integer; AFromDPI: integer): integer; begin result := MulDiv(AValue, DestCanvasDPI, AFromDPI); end; function TBGRAThemeSurface.ScaleForBitmap(AValue: integer; AFromDPI: integer): integer; begin result := MulDiv(AValue, BitmapDPI, AFromDPI); end; function TBGRAThemeSurface.ScaleForBitmap(const ARect: TRect; AFromDPI: integer): TRect; begin result.Left := ScaleForBitmap(ARect.Left, AFromDPI); result.Top := ScaleForBitmap(ARect.Top, AFromDPI); result.Right := ScaleForBitmap(ARect.Right, AFromDPI); result.Bottom := ScaleForBitmap(ARect.Bottom, AFromDPI); end; { TBGRATheme } function TBGRATheme.GetThemedControl(AIndex: integer): TBGRAThemeControl; begin result := TBGRAThemeControl(FThemedControls[AIndex]); end; function TBGRATheme.GetThemedControlCount: integer; begin result := FThemedControls.Count; end; procedure TBGRATheme.AddThemedControl(AControl: TBGRAThemeControl); begin if FThemedControls.IndexOf(AControl) = -1 then FThemedControls.Add(AControl); end; procedure TBGRATheme.RemoveThemedControl(AControl: TBGRAThemeControl); begin FThemedControls.Remove(AControl); end; constructor TBGRATheme.Create(AOwner: TComponent); begin inherited Create(AOwner); FThemedControls := TList.Create; end; destructor TBGRATheme.Destroy; var i: integer; begin for i := ThemedControlCount-1 downto 0 do ThemedControl[i].Theme := nil; FThemedControls.Free; inherited Destroy; end; procedure TBGRATheme.InvalidateThemedControls; var i: Integer; begin for i := 0 to ThemedControlCount-1 do ThemedControl[i].Invalidate; end; function TBGRATheme.PreferredButtonWidth(const hasGlyph: boolean): Integer; begin Result := 125; end; function TBGRATheme.PreferredButtonHeight(const hasGlyph: boolean): Integer; begin Result := 35; end; procedure TBGRATheme.DrawButton(Caption: string; State: TBGRAThemeButtonState; Focused: boolean; ARect: TRect; ASurface: TBGRAThemeSurface; AImageIndex: Integer; AImageList: TBGRASVGImageList); var Style: TTextStyle; begin With ASurface do begin DestCanvas.Font.Color := clBlack; case State of btbsNormal: DestCanvas.Brush.Color := RGBToColor(225, 225, 225); btbsHover: DestCanvas.Brush.Color := RGBToColor(229, 241, 251); btbsActive: DestCanvas.Brush.Color := RGBToColor(204, 228, 247); btbsDisabled: DestCanvas.Brush.Color := RGBToColor(204, 204, 204); end; DestCanvas.Pen.Color := DestCanvas.Brush.Color; DestCanvas.Rectangle(ARect); if Focused then begin DestCanvas.Pen.Color := clBlack; DestCanvas.Rectangle(ARect); end; if Caption <> '' then begin fillchar(Style, sizeof(Style), 0); Style.Alignment := taCenter; Style.Layout := tlCenter; Style.Wordbreak := True; DestCanvas.TextRect(ARect, 0, 0, Caption, Style); end; end; end; procedure TBGRATheme.DrawRadioButton(Caption: string; State: TBGRAThemeButtonState; Focused: boolean; Checked: boolean; ARect: TRect; ASurface: TBGRAThemeSurface); var Style: TTextStyle; Color: TBGRAPixel; begin with ASurface do begin DestCanvas.Font.Color := clBlack; case State of btbsHover: Color := BGRA(0, 120, 215); btbsActive: Color := BGRA(0, 84, 153); btbsDisabled: begin DestCanvas.Font.Color := clGray; Color := BGRA(204, 204, 204); end; else {btbsNormal} Color := BGRABlack; end; BitmapRect := RectWithSize(ARect.Left, ARect.Top, ARect.Height, ARect.Height); Bitmap.FillEllipseAntialias(Bitmap.Height / 2, Bitmap.Height / 2, Bitmap.Height / 2 - 2, Bitmap.Height / 2 - 2, BGRAWhite); Bitmap.EllipseAntialias(Bitmap.Height / 2, Bitmap.Height / 2, Bitmap.Height / 2 - 2, Bitmap.Height / 2 - 2, Color{%H-}, 1); if Checked then Bitmap.FillEllipseAntialias(Bitmap.Height / 2, Bitmap.Height / 2, Bitmap.Height / 4, Bitmap.Height / 4, Color); DrawBitmap; if Caption <> '' then begin fillchar(Style, sizeof(Style), 0); Style.Alignment := taLeftJustify; Style.Layout := tlCenter; Style.Wordbreak := True; DestCanvas.TextRect(Rect(Arect.Height, 0, ARect.Right, ARect.Bottom), ARect.Height, 0, Caption, Style); end; end; end; procedure TBGRATheme.DrawCheckBox(Caption: string; State: TBGRAThemeButtonState; Focused: boolean; Checked: boolean; ARect: TRect; ASurface: TBGRAThemeSurface); var Style: TTextStyle; Bitmap: TBGRABitmap; Color: TBGRAPixel; aleft, atop, aright, abottom: integer; begin with ASurface do begin DestCanvas.Font.Color := clBlack; case State of btbsHover: Color := BGRA(0, 120, 215); btbsActive: Color := BGRA(0, 84, 153); btbsDisabled: begin DestCanvas.Font.Color := clGray; Color := BGRA(204, 204, 204); end; else {btbsNormal} Color := BGRABlack; end; BitmapRect := RectWithSize(ARect.Left, ARect.Top, ARect.Height, ARect.Height); Bitmap.Rectangle(0, 0, Bitmap.Height, Bitmap.Height, Color, BGRAWhite); aleft := 0; aright := Bitmap.Height; atop := 0; abottom := Bitmap.Height; if Checked then Bitmap.DrawPolyLineAntialias(Bitmap.ComputeBezierSpline( [BezierCurve(pointF(aleft + 2, atop + 3), PointF((aleft + aright - 1) / 2, abottom - 3)), BezierCurve(PointF((aleft + aright - 1) / 2, abottom - 3), PointF( (aleft + aright - 1) / 2, (atop * 2 + abottom - 1) / 3), PointF(aright - 2, atop - 2))]), Color, 1.5); DrawBitmap; if Caption <> '' then begin fillchar(Style, sizeof(Style), 0); Style.Alignment := taLeftJustify; Style.Layout := tlCenter; Style.Wordbreak := True; DestCanvas.TextRect(Rect(Arect.Height, 0, ARect.Right, ARect.Bottom), ARect.Height, 0, Caption, Style); end; end; end; var BasicTheme: TBGRATheme; initialization BasicTheme := TBGRATheme.Create(nil); BGRADefaultTheme := BasicTheme; finalization FreeAndNil(BasicTheme); end.