// **************************************************************************************************
//
// Unit Vcl.Styles.ColorTabs
// unit for the VCL Styles Utils
// https://github.com/RRUZ/vcl-styles-utils/
//
// The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License");
// you may not use this file except in compliance with the License. You may obtain a copy of the
// License at http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF
// ANY KIND, either express or implied. See the License for the specific language governing rights
// and limitations under the License.
//
// The Original Code is Vcl.Styles.ColorTabs
//
// The Initial Developer of the Original Code is Rodrigo Ruz V.
// Portions created by Rodrigo Ruz V. are Copyright (C) 2012-2023 Rodrigo Ruz V.
// All Rights Reserved.
//
// **************************************************************************************************
unit Vcl.Styles.ColorTabs;

interface

uses
  Winapi.Messages,
  Vcl.Graphics,
  Vcl.ComCtrls;

type
  TTabSheet = class(Vcl.ComCtrls.TTabSheet)
  private
    procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
  end;

  TTabColorControlStyleHook = class(TTabControlStyleHook)
  private
    class var FUseBorder: Boolean;
    procedure WMEraseBkgnd(var Message: TMessage); message WM_ERASEBKGND;
  protected
    class constructor Create;
    procedure PaintBackground(Canvas: TCanvas); override;
    procedure Paint(Canvas: TCanvas); override;
    procedure DrawTab(Canvas: TCanvas; Index: Integer); override;
    class property UseBorder: Boolean read FUseBorder write FUseBorder;
  end;

implementation

uses
  System.Classes,
  System.SysUtils,
  System.Types,
  System.Rtti,
  System.Generics.Collections,
  Winapi.Windows,
  Vcl.Styles,
  Vcl.Themes,
  Vcl.Controls;

type
  TStyleHookList = TList<TStyleHookClass>;

  TPageControlHelper = class helper for TPageControl
  public
    procedure UpdateTab2(Page: Vcl.ComCtrls.TTabSheet);
  end;

  TWinControlClass = class(TWinControl);
  TCustomTabControlClass = class(TCustomTabControl);

  TTabControlStyleHookHelper = class helper for TTabControlStyleHook
  public
    procedure AngleTextOut2(Canvas: TCanvas; Angle: Integer; X, Y: Integer;
      const Text: string);
  end;

  TStyleHookDictionary = TDictionary<TClass, TStyleHookList>;

  TCustomStyleEngineHelper = Class Helper for TCustomStyleEngine
  public
    class function GetRegisteredStyleHooks: TStyleHookDictionary;
  End;

class function TCustomStyleEngineHelper.GetRegisteredStyleHooks
: TStyleHookDictionary;
{$IF (CompilerVersion >=31)}
var
  p: Pointer;
{$IFEND}
begin
{$IF (CompilerVersion <31)}
  Result := Self.FRegisteredStyleHooks;
{$ELSE}
  {
    TCustomStyleEngine.FRegisteredStyleHooks:
    00651030 3052AA           xor [edx-$56],dl
    00651033 02F7             add dh,bh
    00651035 097623           or [esi+$23],esi
    TCustomStyleEngine.$ClassInitFlag:
    00651038 FFFF             db $ff $ff
    0065103A FFFF             db $ff $ff
    TCustomStyleEngine.FRegSysStylesList:
    0065103C D037             shl [edi],1
  }
{$IFDEF CPUX64}
  p := Pointer(PByte(@Self.FRegSysStylesList) - 24);
{$ELSE}
  p := Pointer(PByte(@Self.FRegSysStylesList) - 12);
{$ENDIF CPUX64}
  Result := TStyleHookDictionary(p^);
{$IFEND}
end;

function GetBorderColorTab: TColor;
begin
  Result := clBlack;
end;

function GetColorTab(Index: Integer): TColor;
Const
  MaxColors = 9;
  Colors: Array [0 .. MaxColors - 1] of TColor = (6512214, 16755712, 8355381,
    1085522, 115885, 1098495, 1735163, 2248434, 4987610);
begin
  Result := Colors[Index mod MaxColors];
end;

function GetColorTextTab(ThemedTab: TThemedTab): TColor;
Const
  ColorSelected = clYellow;
  ColorHot = clGray;
  ColorNormal = clWhite;
begin
  Result := ColorNormal;
  case ThemedTab of
    ttTabItemSelected, ttTabItemLeftEdgeSelected, ttTabItemBothEdgeSelected,
      ttTabItemRightEdgeSelected:
      Result := ColorSelected;

    ttTabItemHot, ttTabItemLeftEdgeHot, ttTabItemBothEdgeHot,
      ttTabItemRightEdgeHot:
      Result := ColorHot;

    ttTabItemNormal, ttTabItemLeftEdgeNormal, ttTabItemBothEdgeNormal,
      ttTabItemRightEdgeNormal:
      Result := ColorNormal;
  end;
end;

function IsStyleHookRegistered(ControlClass: TClass;
  StyleHookClass: TStyleHookClass): Boolean;
var
  List: TStyleHookList;
begin
  Result := False;
  if TCustomStyleEngine.GetRegisteredStyleHooks.ContainsKey(ControlClass) then
  begin
    List := TCustomStyleEngine.GetRegisteredStyleHooks[ControlClass];
    Result := List.IndexOf(StyleHookClass) <> -1;
  end;
end;

{ TTabSheet }

procedure TTabSheet.WMEraseBkgnd(var Message: TWMEraseBkgnd);
var
  LRect: TRect;
  LSize: Integer;
  LCanvas: TCanvas;
begin
  // check if the TTabColorControlStyleHook is registered
  if (not IsStyleHookRegistered(TCustomTabControl, TTabColorControlStyleHook))
    and (not IsStyleHookRegistered(TTabControl, TTabColorControlStyleHook)) then
    inherited
  else if (PageControl <> nil) and StyleServices.Enabled and TStyleManager.IsCustomStyleActive
  then
  begin
    GetWindowRect(Handle, LRect);
    OffsetRect(LRect, -LRect.Left, -LRect.Top);
    LSize := ClientToParent(Point(0, 0)).X;
    InflateRect(LRect, LSize, LSize); // remove border
    // create a TCanvas for erase the background, using the DC of the message
    LCanvas := TCanvas.Create;
    try
      LCanvas.Handle := Message.DC;
      LCanvas.Brush.Color := GetColorTab(TabIndex);
      LCanvas.FillRect(LRect);
    finally
      LCanvas.Handle := 0;
      LCanvas.Free;
    end;

    Message.Result := 1;
    if PageControl.ActivePage <> nil then
      PageControl.UpdateTab2(PageControl.ActivePage);
  end
  else
    inherited;
end;

{ TPageControlHelper }

procedure TPageControlHelper.UpdateTab2(Page: Vcl.ComCtrls.TTabSheet);
begin
{$IF (CompilerVersion <31)}
  Self.UpdateTab(Page);
{$ELSE}
  Self.Tabs[Page.TabIndex] := Page.Caption;
{$IFEND}
end;

{ TTabControlStyleHookHelper }

procedure TTabControlStyleHookHelper.AngleTextOut2(Canvas: TCanvas;
  Angle, X, Y: Integer; const Text: string);
var
  LSavedDC: Integer;
begin
  LSavedDC := SaveDC(Canvas.Handle);
  try
    SetBkMode(Canvas.Handle, TRANSPARENT);
    Canvas.Font.Orientation := Angle;
    Canvas.TextOut(X, Y, Text);
  finally
    RestoreDC(Canvas.Handle, LSavedDC);
  end;
end;

{ TTabColorControlStyleHook }
class constructor TTabColorControlStyleHook.Create;
begin
  FUseBorder := True;
end;

procedure TTabColorControlStyleHook.DrawTab(Canvas: TCanvas; Index: Integer);
var
  LDetails: TThemedElementDetails;
  LImageIndex: Integer;
  LThemedTab: TThemedTab;
  LIconRect: TRect;
  R, LayoutR: TRect;
  LImageW, LImageH, DxImage: Integer;
  LTextX, LTextY: Integer;
  LTextColor: TColor;

  procedure DrawControlText(const S: string; var R: TRect; Flags: Cardinal);
  var
    TextFormat: TTextFormatFlags;
  begin
    Canvas.Font := TWinControlClass(Control).Font;
    TextFormat := TTextFormatFlags(Flags);
    Canvas.Font.Color := LTextColor;
    StyleServices.DrawText(Canvas.Handle, LDetails, S, R, TextFormat, Canvas.Font.Color);
  end;

begin
  if (Images <> nil) and (Index < Images.Count) then
  begin
    LImageW := Images.Width;
    LImageH := Images.Height;
    DxImage := 3;
  end
  else
  begin
    LImageW := 0;
    LImageH := 0;
    DxImage := 0;
  end;

  R := TabRect[Index];
  if R.Left < 0 then
    Exit;

  if TabPosition in [tpTop, tpBottom] then
  begin
    if Index = TabIndex then
      InflateRect(R, 0, 2);
  end
  else if Index = TabIndex then
    Dec(R.Left, 2)
  else
    Dec(R.Right, 2);

  Canvas.Font.Assign(TCustomTabControlClass(Control).Font);
  LayoutR := R;
  LThemedTab := ttTabDontCare;
  // Get the type of the active tab
  case TabPosition of
    tpTop:
      begin
        if Index = TabIndex then
          LThemedTab := ttTabItemSelected
        else if (Index = HotTabIndex) and MouseInControl then
          LThemedTab := ttTabItemHot
        else
          LThemedTab := ttTabItemNormal;
      end;
    tpLeft:
      begin
        if Index = TabIndex then
          LThemedTab := ttTabItemLeftEdgeSelected
        else if (Index = HotTabIndex) and MouseInControl then
          LThemedTab := ttTabItemLeftEdgeHot
        else
          LThemedTab := ttTabItemLeftEdgeNormal;
      end;
    tpBottom:
      begin
        if Index = TabIndex then
          LThemedTab := ttTabItemBothEdgeSelected
        else if (Index = HotTabIndex) and MouseInControl then
          LThemedTab := ttTabItemBothEdgeHot
        else
          LThemedTab := ttTabItemBothEdgeNormal;
      end;
    tpRight:
      begin
        if Index = TabIndex then
          LThemedTab := ttTabItemRightEdgeSelected
        else if (Index = HotTabIndex) and MouseInControl then
          LThemedTab := ttTabItemRightEdgeHot
        else
          LThemedTab := ttTabItemRightEdgeNormal;
      end;
  end;

  // draw the tab
  if StyleServices.Available then
  begin
    LDetails := StyleServices.GetElementDetails(LThemedTab);
    // necesary for  DrawControlText

    if FUseBorder then
    begin
      case TabPosition of
        tpTop:
          begin
            InflateRect(R, -1, 0);
            if TabIndex <> Index then
              R.Bottom := R.Bottom + 1
            else
              R.Bottom := R.Bottom - 1;

            Canvas.Brush.Color := GetBorderColorTab;
            Canvas.FillRect(R);

            if TabIndex = Index then
            begin
              InflateRect(R, -1, -1);
              R.Bottom := R.Bottom + 1;
            end
            else
              InflateRect(R, -1, -1);
          end;

        tpBottom:
          begin
            InflateRect(R, -1, 0);
            if TabIndex <> Index then
              R.Bottom := R.Bottom + 1
            else
              R.Top := R.Top + 3;

            Canvas.Brush.Color := GetBorderColorTab;
            Canvas.FillRect(R);

            if TabIndex = Index then
            begin
              InflateRect(R, -1, 0);
              R.Bottom := R.Bottom - 1;
            end
            else
              InflateRect(R, -1, -1);
          end;

        tpLeft:
          begin
            InflateRect(R, 0, -1);

            if TabIndex <> Index then
              R.Left := R.Left + 1
            else
              R.Right := R.Right - 1;

            Canvas.Brush.Color := GetBorderColorTab;
            Canvas.FillRect(R);

            if TabIndex = Index then
            begin
              InflateRect(R, -1, -1);
              R.Right := R.Right + 1;
            end
            else
              InflateRect(R, -1, -1);

          end;

        tpRight:
          begin
            InflateRect(R, 0, -1);

            if TabIndex <> Index then
              // R.Left:=R.Left+1
            else
              R.Left := R.Left + 3;

            Canvas.Brush.Color := GetBorderColorTab;
            Canvas.FillRect(R);

            if TabIndex = Index then
            begin
              InflateRect(R, -1, -1);
              R.Left := R.Left - 1;
            end
            else
              InflateRect(R, -1, -1);

          end;

      end;

      Canvas.Brush.Color := GetColorTab(Index);
      Canvas.FillRect(R);
    end
    else
    Begin
      InflateRect(R, -1, 0);
      // adjust the size of the tab creating blanks space between the tabs
      Canvas.Brush.Color := GetColorTab(Index);
      Canvas.FillRect(R);
    end;

  end;

  // get the index of the image (icon)
  if Control is TCustomTabControl then
    LImageIndex := TCustomTabControlClass(Control).GetImageIndex(Index)
  else
    LImageIndex := Index;

  // draw the image
  if (Images <> nil) and (LImageIndex >= 0) and (LImageIndex < Images.Count)
  then
  begin
    LIconRect := LayoutR;
    case TabPosition of
      tpTop, tpBottom:
        begin
          LIconRect.Left := LIconRect.Left + DxImage;
          LIconRect.Right := LIconRect.Left + LImageW;
          LayoutR.Left := LIconRect.Right;
          LIconRect.Top := LIconRect.Top + (LIconRect.Bottom - LIconRect.Top)
            div 2 - LImageH div 2;
          if (TabPosition = tpTop) and (Index = TabIndex) then
            OffsetRect(LIconRect, 0, -1)
          else if (TabPosition = tpBottom) and (Index = TabIndex) then
            OffsetRect(LIconRect, 0, 1);
        end;
      tpLeft:
        begin
          LIconRect.Bottom := LIconRect.Bottom - DxImage;
          LIconRect.Top := LIconRect.Bottom - LImageH;
          LayoutR.Bottom := LIconRect.Top;
          LIconRect.Left := LIconRect.Left + (LIconRect.Right - LIconRect.Left)
            div 2 - LImageW div 2;
        end;
      tpRight:
        begin
          LIconRect.Top := LIconRect.Top + DxImage;
          LIconRect.Bottom := LIconRect.Top + LImageH;
          LayoutR.Top := LIconRect.Bottom;
          LIconRect.Left := LIconRect.Left + (LIconRect.Right - LIconRect.Left)
            div 2 - LImageW div 2;
        end;
    end;
    if StyleServices.Available then
      StyleServices.DrawIcon(Canvas.Handle, LDetails, LIconRect, Images.Handle,
        LImageIndex);
  end;

  // draw the text of the tab
  if StyleServices.Available then
  begin
    LTextColor := GetColorTextTab(LThemedTab);

    if (TabPosition = tpTop) and (Index = TabIndex) then
      OffsetRect(LayoutR, 0, -1)
    else if (TabPosition = tpBottom) and (Index = TabIndex) then
      OffsetRect(LayoutR, 0, 1);

    if TabPosition = tpLeft then
    begin
      LTextX := LayoutR.Left + (LayoutR.Right - LayoutR.Left) div 2 -
        Canvas.TextHeight(Tabs[Index]) div 2;
      LTextY := LayoutR.Top + (LayoutR.Bottom - LayoutR.Top) div 2 +
        Canvas.TextWidth(Tabs[Index]) div 2;
      Canvas.Font.Color := LTextColor;
      AngleTextOut2(Canvas, 900, LTextX, LTextY, Tabs[Index]);
    end
    else if TabPosition = tpRight then
    begin
      LTextX := LayoutR.Left + (LayoutR.Right - LayoutR.Left) div 2 +
        Canvas.TextHeight(Tabs[Index]) div 2;
      LTextY := LayoutR.Top + (LayoutR.Bottom - LayoutR.Top) div 2 -
        Canvas.TextWidth(Tabs[Index]) div 2;
      Canvas.Font.Color := LTextColor;
      AngleTextOut2(Canvas, -900, LTextX, LTextY, Tabs[Index]);
    end
    else
      DrawControlText(Tabs[Index], LayoutR, DT_VCENTER or DT_CENTER or
        DT_SINGLELINE or DT_NOCLIP);
  end;
end;

procedure TTabColorControlStyleHook.Paint(Canvas: TCanvas);
var
  LRect: TRect;
  LIndex: Integer;
  SavedDC: Integer;
begin
  SavedDC := SaveDC(Canvas.Handle);
  try
    LRect := DisplayRect;
    ExcludeClipRect(Canvas.Handle, LRect.Left, LRect.Top, LRect.Right,
      LRect.Bottom);
    PaintBackground(Canvas);
  finally
    RestoreDC(Canvas.Handle, SavedDC);
  end;

  // Draw tabs , except the active
  for LIndex := 0 to TabCount - 1 do
  begin
    if LIndex = TabIndex then
      Continue;
    DrawTab(Canvas, LIndex);
  end;

  // Draw the body
  case TabPosition of
    tpTop:
      InflateRect(LRect, Control.Width - LRect.Right, Control.Height - LRect.Bottom);
    tpLeft:
      InflateRect(LRect, Control.Width - LRect.Right, Control.Height - LRect.Bottom);
    tpBottom:
      InflateRect(LRect, LRect.Left, LRect.Top);
    tpRight:
      InflateRect(LRect, LRect.Left, LRect.Top);
  end;

  if StyleServices.Available then
  begin
    if FUseBorder then
    begin
      Canvas.Brush.Color := GetBorderColorTab;
      Canvas.Rectangle(LRect.Left, LRect.Top, LRect.Right, LRect.Bottom);

      InflateRect(LRect, -1, -1);
      Canvas.Brush.Color := GetColorTab(TabIndex);
      Canvas.FillRect(LRect);
    end
    else
    begin
      Canvas.Brush.Color := GetColorTab(TabIndex);
      Canvas.FillRect(LRect);
    end;
  end;

  // Draw active tab
  if TabIndex >= 0 then
    DrawTab(Canvas, TabIndex);

  // paint the controls of the tab
  TWinControlClass(Control).PaintControls(Canvas.Handle, nil);
end;

procedure TTabColorControlStyleHook.PaintBackground(Canvas: TCanvas);
var
  LColor: TColor;
begin
  if StyleServices.Available then
  begin

    if Control.Parent is TTabSheet then
      LColor := GetColorTab(TTabSheet(Control.Parent).PageIndex)
    else
      LColor := StyleServices.GetSystemColor(clWindowFrame);

    Canvas.Brush.Color := LColor;
    Canvas.FillRect(Control.ClientRect);
  end;
end;

procedure TTabColorControlStyleHook.WMEraseBkgnd(var Message: TMessage);
var
  LCanvas: TCanvas;
begin
  if (Message.LParam = 1) and StyleServices.Available then
  begin
    LCanvas := TCanvas.Create;
    try
      LCanvas.Handle := HDC(Message.WParam);
      LCanvas.Brush.Color := GetColorTab(TabIndex);
      LCanvas.FillRect(Control.ClientRect);
    finally
      LCanvas.Handle := 0;
      LCanvas.Free;
    end;
  end;
  Message.Result := 1;
  Handled := True;
end;

end.
