unit HexaColorPicker;

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}

interface

//{$I mxs.inc}

uses
  LCLIntf, LCLType, LMessages, SysUtils, Classes, Controls, Graphics, 
  Forms, Themes, Math,
  HTMLColors, mbBasicPicker;

const
  CustomCell = -2;
  NoCell = -1;

type
  TMarker = (smArrow, smRect);

  TCombEntry = record
    Position: TPoint;
    Color: COLORREF;
    TabIndex: integer;
  end;

  TCombArray = array of TCombEntry;

  TFloatPoint = record
    X, Y: Extended;
  end;

  TRGBrec = record
    Red, Green, Blue: Single;
  end;

  TSelectionMode = (smNone, smColor, smBW, smRamp);

  THexaColorPicker = class(TmbBasicPicker)
  private
    FIncrement: integer;
    FSelectedCombIndex: integer;
    mX, mY: integer;
    FHintFormat: string;
    FUnderCursor: TColor;
    //FOnChange,
    FOnIntensityChange: TNotifyEvent;
    FCurrentColor: TColor;
    FSelectedIndex: Integer;
    FColorCombRect, FBWCombRect, FSliderRect, FCustomColorRect: TRect;
    FCombSize, FLevels: Integer;
    FBWCombs, FColorCombs: TCombArray;
    FCombCorners: array[0..5] of TFloatPoint;
    FCenterColor: TRGBrec;
    FCenterIntensity: Single;
    FSliderWidth: integer;
    FCustomIndex: Integer;  // If FSelectedIndex contains CustomCell then this index shows
                            // which index in the custom area has been selected.
                            // Positive values indicate the color comb and negative values
                            // indicate the B&W combs (complement). This value is offset with
                            // 1 to use index 0 to show no selection.
    FRadius: Integer;
    FSelectionMode: TSelectionMode;
    FSliderVisible: boolean;
    FMarker: TMarker;
    FNewArrowStyle: boolean;
    FIntensityText: string;
    procedure CalculateCombLayout;
    procedure ChangeIntensity(increase: boolean);
    procedure DrawAll;
    procedure DrawComb(ACanvas: TCanvas; X, Y, Size: Integer);
    procedure DrawCombControls(ACanvas: TCanvas);
    procedure EndSelection;
    procedure EnumerateCombs;
    function FindBWArea(X, Y: Integer): Integer;
    function FindColorArea(X, Y: Integer): Integer;
    function GetIntensity: integer;
    function GetNextCombIndex(i: integer): integer;
    function GetPreviousCombIndex(i: integer): integer;
    procedure HandleCustomColors(var Message: TLMMouse);
    function HandleBWArea(const Message: TLMMouse): Boolean;
    function HandleColorComb(const Message: TLMMouse): Boolean;
    function HandleSlider(const Message: TLMMouse): Boolean;
    procedure Initialize;
    function PtInComb(Comb: TCombEntry; P: TPoint; Scale: Integer): Boolean;
    procedure SetIntensity(v: integer);
    procedure SetNewArrowStyle(Value: boolean);
    procedure SetMarker(Value: TMarker);
    procedure SetRadius(r: integer);
    procedure SetSliderVisible(Value: boolean);
    procedure SetSliderWidth(w: integer);
    function SelectAvailableColor(Color: TColor): boolean;
    procedure SelectColor(Color: TColor);
  protected
    function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure Paint; override;
    procedure Resize; override;
    procedure SetSelectedColor(Value: TColor); override;
    procedure CMHintShow(var Message: TLMessage); message CM_HINTSHOW;
    procedure WMLButtonDown(var Message: TLMLButtonDown); message LM_LBUTTONDOWN;
    procedure WMLButtonUp(var Message: TLMLButtonUp); message LM_LBUTTONUP;
    procedure WMMouseMove(var Message: TLMMouseMove); message LM_MOUSEMOVE;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function GetColorAtPoint(X, Y: integer): TColor; override;
    function GetColorUnderCursor: TColor; override;
    function GetHexColorUnderCursor: string; override;
    function GetHexColorAtPoint(X, Y: integer): string;
    function GetSelectedCombIndex: integer;
    procedure SelectCombIndex(i: integer);
    property ColorUnderCursor: TColor read GetColorUnderCursor;
  published
    property Align;
    property Anchors;
    property HintFormat: string read FHintFormat write FHintFormat;
    property Intensity: integer read GetIntensity write SetIntensity default 100;
    property IntensityIncrement: integer read FIncrement write FIncrement default 1;
    property IntensityText: string read FIntensityText write FIntensityText;
    property NewArrowStyle: boolean read FNewArrowStyle write SetNewArrowStyle default false;
    property SelectedColor: TColor read FCurrentColor write SetSelectedColor default clBlack;
    property SliderVisible: boolean read FSliderVisible write SetSliderVisible default true;
    property SliderWidth: integer read FSliderWidth write SetSliderWidth default 12;
    property SliderMarker: TMarker read FMarker write SetMarker default smArrow;
    property ShowHint default true;
    property TabStop default true;
    property Visible;
    property Enabled;
    property PopupMenu;
    property TabOrder;
    property Color;
    property ParentColor;
    property DragCursor;
    property DragMode;
    property DragKind;
    property Constraints;
    property OnChange; //: TNotifyEvent read FOnChange write FOnChange;
    property OnIntensityChange: TNotifyEvent read FOnIntensityChange write FOnIntensityChange;
    property OnDblClick;
    property OnContextPopup;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseWheelUp;
    property OnMouseWheelDown;
    property OnKeyDown;
    property OnKeyPress;
    property OnKeyUp;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnResize;
    property OnStartDrag;
  end;

  const
    DefCenterColor: TRGBrec = (Red: 1; Green: 1; Blue: 1);  // White
    DefColors: array[0..5] of TRGBrec = (
      (Red: 1; Green: 0; Blue: 1),     // Magenta
      (Red: 1; Green: 0; Blue: 0),     // Red
      (Red: 1; Green: 1; Blue: 0),     // Yellow
      (Red: 0; Green: 1; Blue: 0),     // Green
      (Red: 0; Green: 1; Blue: 1),     // Cyan
      (Red: 0; Green: 0; Blue: 1)      // Blue
    );
    DefCenter: TFloatPoint = (X: 0; Y: 0);


implementation

uses
  PalUtils, mbUtils;

{ THexaColorPicker }

constructor THexaColorPicker.Create(AOwner: TComponent);
begin
  inherited;
  //ControlStyle := ControlStyle - [csAcceptsControls] + [csOpaque];
  FRadius := 90;
  FSliderWidth := 12;
  DoubleBuffered := true;
  SetInitialBounds(0, 0, 204, 204);
  Constraints.MinHeight := 85;
  Constraints.MinWidth := 93;
  TabStop := true;
  FSelectedCombIndex := 0;
  FHintFormat := 'RGB(%r, %g, %b)'#13'Hex: #%hex';
  ShowHint := True;
  FSliderVisible := true;
  FMarker := smArrow;
  FNewArrowStyle := false;
  Initialize;
  DrawAll;
  FIntensityText := 'Intensity';
  {
  MaxHue := 360;
  MaxLum := 255;
  MaxSat := 255;
  }
end;

destructor THexaColorPicker.Destroy;
begin
  FBWCombs := nil;
  FColorCombs := nil;
  // FBufferBmp.Free;  is already destroyed by ancestor TmbBasicPicker
  inherited;
end;

procedure THexaColorPicker.ChangeIntensity(increase: boolean);
var
  i: integer;
begin
  i := round(FCenterIntensity * 100);
  if increase then
  begin
    Inc(i, FIncrement);
    if i > 100 then i := 100;
    SetIntensity(i);
  end
  else
  begin
    Dec(i, FIncrement);
    if i < 0 then i := 0;
    SetIntensity(i);
  end;
end;

function THexaColorPicker.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
  MousePos: TPoint): Boolean;
begin
  Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
  if not Result then
  begin
    Result := True;
    ChangeIntensity(WheelDelta > 0);
  end;
end;

procedure THexaColorPicker.DrawComb(ACanvas: TCanvas; X, Y: Integer; Size: Integer);
var
  I: Integer;
  P: array[0..5] of TPoint;
begin
  for I := 0 to 5 do
  begin
    P[I].X := Round(FCombCorners[I].X * Size + X);
    P[I].Y := Round(FCombCorners[I].Y * Size + Y);
  end;
  ACanvas.Polygon(P);
end;

procedure THexaColorPicker.DrawCombControls(ACanvas: TCanvas);
var
  I, Index: Integer;
  XOffs, YOffs, Count: Integer;
  OffScreen: TBitmap;
  R: TRect;
begin
  OffScreen := TBitmap.Create;
  try
    OffScreen.Width := Width;
    OffScreen.Height := HeightOfRect(FColorCombRect) + HeightOfRect(FBWCombRect);

    //Parent background
    if Color = clDefault then
    begin
      Offscreen.Transparent := true;
      Offscreen.TransparentColor := clForm;
      Offscreen.Canvas.Brush.Color := clForm
    end else
      OffScreen.Canvas.Brush.Color := Color;
    OffScreen.Canvas.FillRect(OffScreen.Canvas.ClipRect);

    with OffScreen.Canvas do
    begin
      Pen.Style := psClear;

      // draw color combs from FColorCombs array
      XOffs := FRadius + FColorCombRect.Left;
      YOffs := FRadius + FColorCombRect.Top;

      // draw the combs
      for I := 0 to High(FColorCombs) do
      begin
        Brush.Color := FColorCombs[I].Color;
        Pen.Mode := pmCopy; // the pen is set here so there are no gaps between the combs
        Pen.Style := psSolid;
        Pen.Color := FColorCombs[I].Color;
        DrawComb(OffScreen.Canvas, FColorCombs[I].Position.X + XOffs, FColorCombs[I].Position.Y + YOffs, FCombSize);
      end;

      // mark selected comb
      if FCustomIndex > 0 then
      begin
        Index := FCustomIndex - 1;
        FSelectedCombIndex := index;
        Pen.Style := psSolid;
        {
        Pen.Mode := pmXOR;
        Pen.Color := clWhite;
        }
        Pen.Color := HighContrastColor(FColorCombs[Index].Color);
        Pen.Width := 2;
        Brush.Style := bsClear;
        DrawComb(OffScreen.Canvas, FColorCombs[Index].Position.X + XOffs, FColorCombs[Index].Position.Y + YOffs, FCombSize);
        Pen.Style := psClear;
        Pen.Mode := pmCopy;
        Pen.Width := 1;
      end;

      // draw white-to-black combs
      XOffs := FColorCombRect.Left;
      YOffs := FColorCombRect.Bottom - 4;
      // brush is automatically reset to bsSolid
      for I := 0 to High(FBWCombs) do
      begin
        Pen.Mode := pmCopy; // the pen is set here so there are no gaps between the combs
        Pen.Style := psSolid;
        Pen.Color := FBWCombs[I].Color;
        Brush.Color := FBWCombs[I].Color;
        if I in [0, High(FBWCombs)] then
        begin
          if Pen.Color = clWhite then   // "white" needs a border if background is white as well
            Pen.Color := clGray;
          DrawComb(OffScreen.Canvas, FBWCombs[I].Position.X + XOffs, FBWCombs[I].Position.Y + YOffs, 2 * FCombSize)
        end else
          DrawComb(OffScreen.Canvas, FBWCombs[I].Position.X + XOffs, FBWCombs[I].Position.Y + YOffs, FCombSize);
      end;

      // mark selected comb
      if FCustomIndex < 0 then
      begin
        Index := -(FCustomIndex + 1);
        if index < 0 then
          FSelectedCombIndex := Index
        else
          FSelectedCombIndex := -index;
        Pen.Style := psSolid;
        {
        Pen.Mode := pmXOR;
        Pen.Color := clWhite;
        }
        Pen.Mode := pmCopy;
        Pen.Color := HighContrastColor(FBWCombs[Index].Color);
        Pen.Width := 2;
        Brush.Style := bsClear;
        if Index in [0, High(FBWCombs)] then
        begin
          if Index = High(FBWCombs) then begin
            Pen.Color := rgb(254, 254, 254); //clWhite;
            Pen.Mode := pmXOR;
          end;
          if ((FColorCombs[0].Color = Cardinal(clWhite)) and (Index = 0)) or
             ((FColorCombs[0].Color = Cardinal(clBlack)) and (Index = High(FBWCombs)))
          then
            DrawComb(OffScreen.Canvas, FRadius + FColorCombRect.Left, FRadius + FColorCombRect.Top, FCombSize); // mark white or black center
          DrawComb(OffScreen.Canvas, FBWCombs[Index].Position.X + XOffs, FBWCombs[Index].Position.Y + YOffs, 2 * FCombSize);
        end
        else
          DrawComb(OffScreen.Canvas, FBWCombs[Index].Position.X + XOffs, FBWCombs[Index].Position.Y + YOffs, FCombSize);
        Pen.Style := psClear;
        Pen.Mode := pmCopy;
        Pen.Width := 1;
      end;

      // Slider
      if FSliderVisible then
      begin
        // center-color trackbar
        R := FSliderRect;
        R.Right := R.Left + FSliderWidth;
        Pen.Style := psSolid;
        GradientFill(R, clWhite, clBlack, gdVertical);

        // draw marker
        Count := FSliderRect.Bottom - FSliderRect.Top - 1;
        XOffs := FSliderRect.Left + FSliderWidth + 1;
        YOffs := FSliderRect.Top + Round(Count * (1 - FCenterIntensity));;
        Pen.Color := clBlack;
        case FMarker of
          smArrow:
            begin
              if not FNewArrowStyle then
              begin
                Brush.Color := clBlack;
                Polygon([
                  Point(XOffs, YOffs),
                  Point(XOffs + 6, YOffs - 4),
                  Point(XOffs + 6, YOffs + 4)
                ])
              end
              else
              begin
                Brush.Color := clWhite;
                Pen.Color := clBtnShadow;
                Polygon([
                  Point(XOffs, YOffs),
                  Point(XOffs + 4, YOffs - 4),
                  Point(XOffs + 6, YOffs - 4),
                  Point(XOffs + 7, YOffs - 3),
                  Point(XOffs + 7, YOffs + 3),
                  Point(XOffs + 6, YOffs + 4),
                  Point(XOffs + 4, YOffs + 4)]);
              end;
            end;
          smRect:
            begin
              Brush.Style := bsClear;
              Pen.Mode := pmNot;
              Rectangle(XOffs - FSliderWidth - 4, YOffs - 3, XOffs + 2, YOffs + 3);
              Pen.Mode := pmCopy;
              Brush.Style := bsSolid;
            end;
        end;  // case
        Pen.Style := psClear;
      end;
    end;
    ACanvas.Draw(0, 0, OffScreen);
  finally
    Offscreen.Free;
  end;
  EnumerateCombs;
end;

// Looks for a comb at position (X, Y) in the black&white area.
// Result is -1 if nothing could be found else the index of the particular comb
// into FBWCombs.
function THexaColorPicker.FindBWArea(X, Y: Integer): Integer;
var
  I, Scale: Integer;
  Pt: TPoint;
begin
  Result := -1;
  Pt := Point(X - FBWCombRect.Left, Y - FBWCombRect.Top);
  for I := 0 to High(FBWCombs) do
  begin
    if I in [0, High(FBWCombs)] then
      Scale := FCombSize
    else
      Scale := FCombSize div 2;
    if PtInComb(FBWCombs[I], Pt, Scale) then
    begin
      Result := I;
      Break;
    end;
  end;
end;

// Looks for a comb at position (X, Y) in the custom color area.
// Result is -1 if nothing could be found else the index of the particular comb
// into FColorCombs.
function THexaColorPicker.FindColorArea(X, Y: Integer): Integer;
var
  I: Integer;
  Pt: TPoint;
begin
  Result := -1;
  Pt := Point(X - (FRadius + FColorCombRect.Left), Y - (FRadius + FColorCombRect.Top));
  for I := 0 to High(FColorCombs) do
  begin
    if PtInComb(FColorCombs[I], Pt, FCombSize div 2) then
    begin
      Result := I;
      Break;
    end;
  end;
end;

function THexaColorPicker.GetIntensity: integer;
begin
  Result := round(FCenterIntensity * 100);
end;

function THexaColorPicker.GetNextCombIndex(i: integer): integer;
begin
  if i = 127 then
    Result := -1
  else
  if i = -15 then
    Result := 1
  else
  if i > 0 then
    Result := i + 1
  else
    Result := i - 1;
end;

function THexaColorPicker.GetPreviousCombIndex(i: integer): integer;
begin
  if i = 1 then
    Result := -15
  else
  if i = -1 then
    Result := 127
  else
  if i > 0 then
    Result := i - 1
  else
    Result := i + 1;
end;

function THexaColorPicker.GetSelectedCombIndex: integer;
begin
  if FSelectedCombIndex < 0 then
    Result := FBWCombs[-FSelectedCombIndex].TabIndex
  else
    Result := FColorCombs[FSelectedCombIndex].TabIndex;
end;

// determines whether the mouse position is within the B&W comb area and acts accordingly
function THexaColorPicker.HandleBWArea(const Message: TLMMouse): Boolean;
var
  Index: Integer;
  Shift: TShiftState;
begin
  Result := PtInRect(FBWCombRect, Point(Message.XPos, Message.YPos)) and (FSelectionMode in [smNone, smBW]);
  if Result then
  begin
    Shift := KeysToShiftState(Message.Keys);
    if ssLeft in Shift then
    begin
      FSelectionMode := smBW;
      Index := FindBWArea(Message.XPos, Message.YPos);
      if Index > -1 then
      begin
        // remove selection comb if it was previously in color comb
        if FCustomIndex > 0 then InvalidateRect(Handle, @FColorCombRect, False);
        if FCustomIndex <> -(Index + 1) then
        begin
          FCustomIndex := -(Index + 1);
          InvalidateRect(Handle, @FBWCombRect, False);
          InvalidateRect(Handle, @FCustomColorRect, False);
          EndSelection;
        end;
      end
      else
        Result := False;
    end;
  end;
end;

// determines whether the mouse position is within the color comb area and acts accordingly
function THexaColorPicker.HandleColorComb(const Message: TLMMouse): Boolean;
var
  Index: Integer;
  Shift: TShiftState;
begin
  Result := PtInRect(FColorCombRect, Point(Message.XPos, Message.YPos)) and (FSelectionMode in [smNone, smColor]);
  if Result then
  begin
    Shift := KeysToShiftState(Message.Keys);
    if ssLeft in Shift then
    begin
      FSelectionMode := smColor;
      Index := FindColorArea(Message.XPos, Message.YPos);
      if Index > -1 then
      begin
        // remove selection comb if it was previously in b&w comb
        if FCustomIndex < 0 then InvalidateRect(Handle, @FBWCombRect, False);
        if FCustomIndex <> (Index + 1) then
        begin
          FCustomIndex := Index + 1;
          InvalidateRect(Handle, @FColorCombRect, False);
          InvalidateRect(Handle, @FCustomColorRect, False);
          EndSelection;
        end;
      end
      else
        Result := False;
    end;
  end;
end;

procedure THexaColorPicker.HandleCustomColors(
  var Message: {$IFDEF FPC}TLMMouse{$ELSE}TWMMouse{$ENDIF});
begin
  if not HandleSlider(Message) then
    if not HandleBWArea(Message) then
      HandleColorComb(Message);
end;

// determines whether the mouse position is within the slider area and acts accordingly
function THexaColorPicker.HandleSlider(const Message: TLMMouse): Boolean;
var
  Shift: TShiftState;
  dY: Integer;
  R: TRect;
begin
  if not FSliderVisible then
  begin
    Result := false;
    Exit;
  end;

  Result :=
    (PtInRect(FSliderRect, Point(Message.XPos, Message.YPos))
      and (FSelectionMode = smNone))
    or
    ((Message.XPos >= FSliderRect.Left) and (Message.XPos <= FSliderRect.Right)
      and (FSelectionMode = smRamp));

  if Result then
  begin
    Shift := KeysToShiftState(Message.Keys);
    if ssLeft in Shift then
    begin
      FSelectionMode := smRamp;
      dY := FSliderRect.Bottom - FSliderRect.Top;
      FCenterIntensity := 1 - (Message.YPos - FSliderRect.Top) / dY;
      if FCenterIntensity < 0 then FCenterIntensity := 0;
      if FCenterIntensity > 1 then FCenterIntensity := 1;
      FCenterColor.Red := DefCenterColor.Red * FCenterIntensity;
      FCenterColor.Green := DefCenterColor.Green * FCenterIntensity;
      FCenterColor.Blue := DefCenterColor.Blue * FCenterIntensity;
      R := FSliderRect;
      Dec(R.Top, 3);
      Inc(R.Bottom, 3);
      Inc(R.Left, 10);
      InvalidateRect(Handle, @R, False);
      FColorCombs := nil;
      InvalidateRect(Handle, @FColorCombRect, False);
      InvalidateRect(Handle, @FCustomColorRect, False);
      CalculateCombLayout;
      EndSelection;
      if Assigned(FOnIntensityChange) then
        FOnIntensityChange(Self);
    end;
  end;
end;

procedure THexaColorPicker.Initialize;
var
  I: Integer;
begin
  FSelectedIndex := NoCell;
  for I := 0 to 5 do
  begin
    FCombCorners[I].X := 0.5 * cos(Pi * (90 - I * 60) / 180);
    FCombCorners[I].Y := 0.5 * sin(Pi * (90 - I * 60) / 180);
  end;
  FLevels := 7;
  FCombSize := Round(FRadius / (FLevels - 1));
  FCenterColor := DefCenterColor;
  FIncrement := 1;
  FCenterIntensity := 1;
end;

procedure THexaColorPicker.KeyDown(var Key: Word; Shift: TShiftState);
var
  eraseKey: Boolean;
begin
  eraseKey := true;
  if ssCtrl in Shift then
    case Key of
      VK_LEFT: SetSelectedColor(clWhite);
      VK_RIGHT: SetSelectedColor(clBlack);
      VK_UP: if FSliderVisible then SetIntensity(100);
      VK_DOWN: if FSliderVisible then SetIntensity(0);
    else
      eraseKey := false;
    end
  else
    case Key of
      VK_LEFT: SelectCombIndex(GetPreviousCombIndex(GetSelectedCombIndex));
      VK_RIGHT: SelectCombIndex(GetNextCombIndex(GetSelectedCombIndex));
      VK_UP: if FSliderVisible then ChangeIntensity(true);
      VK_DOWN: if FSliderVisible then ChangeIntensity(false);
     else
       eraseKey := false;
    end;
  if eraseKey then
    Key := 0;
  inherited;
end;

procedure THexaColorPicker.Paint;
begin
  PaintParentBack(Canvas);
  if FColorCombs = nil then
    CalculateCombLayout;
  DrawCombControls(Canvas);
end;

function THexaColorPicker.PtInComb(Comb: TCombEntry; P: TPoint; Scale: Integer): Boolean;
begin
  Result := (Sqr(Comb.Position.X - P.X) + Sqr(Comb.Position.Y - P.Y)) <= Scale * Scale;
end;

procedure THexaColorPicker.DrawAll;
var
  WinTop: integer;
begin
  WinTop := - FRadius div 8; // use 10 instead of 8 if the top has been cut
  FCombSize := Round(1 + FRadius / (FLevels - 1));
  FColorCombRect := Rect(0, WinTop, 2 * FRadius, 2 * FRadius + WinTop);
  FBWCombRect := Rect(
    FColorCombRect.Left,
    FColorCombRect.Bottom - 4,
    Round(17 * FCombSize * cos(Pi / 6) / 2) {%H-}+ 6 * FCombSize,
    FColorCombRect.Bottom + 2 * FCombSize
  );
  if FSliderVisible then
    FSliderRect := Rect(FColorCombRect.Right, FCombSize, FColorCombRect.Right + 10 + FSliderWidth, FColorCombRect.Bottom - FCombSize)
//    FSliderRect := Rect(FColorCombRect.Right, FColorCombRect.Top, FColorCombRect.Right + 10 + FSliderWidth, FColorCombRect.Bottom)
  else
    FSliderRect := Rect(-1, -1, -1, -1);
end;

// fills arrays with centers and colors for the custom color and black & white combs,
// these arrays are used to quickly draw the combx and do hit tests

function RGBFromFloat(Color: TRGBrec): COLORREF;
begin
  Result := RGB(Round(255 * Color.Red), Round(255 * Color.Green), Round(255 * Color.Blue));
end;

{function TRGBrecFromTColor(Color: TColor): TRGBrec;
begin
 Result.Red := GetRValue(Color)/255;
 Result.Green := GetGValue(Color)/255;
 Result.Blue := GetBValue(Color)/255;
end;}

procedure THexaColorPicker.CalculateCombLayout;

  function GrayFromIntensity(Intensity: Byte): COLORREF;
  begin
    Result := RGB(Intensity, Intensity, Intensity);
  end;

var
  I, J, Level, CurrentIndex, CombCount: Cardinal;
  CurrentColor: TRGBrec;
  CurrentPos: TFloatPoint;
  Scale: Extended;
  // triangle vars
  Pos1, Pos2, dPos1, dPos2, dPos: TFloatPoint;
  Color1, Color2, dColor1, dColor2, dColor: TRGBrec;
begin
  // this ensures the radius and comb size is set correctly
//  HandleNeeded;
  if FLevels < 1 then FLevels := 1;
  // To draw perfectly aligned combs we split the final comb into six triangles (sextants)
  // and calculate each separately. The center comb is stored as first entry in the array
  // and will not considered twice (as with the other shared combs too).
  //
  // The way used here for calculation of the layout seems a bit complicated, but works
  // correctly for all cases (even if the comb corners are rotated).
  // initialization
  CurrentIndex := 0;
  CurrentColor := FCenterColor;
  // number of combs can be calculated by:
  // 1 level: 1 comb (the center)
  // 2 levels: 1 comb + 6 combs
  // 3 levels: 1 comb + 1 * 6 combs + 2 * 6 combs
  // n levels: 1 combs + 1 * 6 combs + 2 * 6 combs + .. + (n-1) * 6 combs
  // this equals to 1 + 6 * (1 + 2 + 3 + .. + (n-1)), by using Gauss' famous formula we get:
  // Count = 1 + 6 * (((n-1) * n) / 2)
  // Because there's always an even number involved (either n or n-1) we can use an integer div
  // instead of a float div here...
  CombCount := 1 + 6 * (((FLevels - 1) * FLevels) div 2);
  SetLength(FColorCombs, CombCount);
  // store center values
  FColorCombs[CurrentIndex].Position := Point(0, 0);
  FColorCombs[CurrentIndex].Color := RGBFromFloat(CurrentColor);
  Inc(CurrentIndex);
  // go out off here if there are not further levels to draw
  if FLevels < 2 then Exit;
  // now go for each sextant, the generic corners have been calculated already at creation
  // time for a comb with diameter 1
  //              ------
  //             /\  1 /\
  //            /  \  /  \
  //           / 2  \/  0 \
  //           -----------
  //           \ 3  /\  5 /
  //            \  /  \  /
  //             \/  4 \/
  //              ------
  for I := 0 to 5 do
  begin
    // initialize triangle corner values
    //
    //                center (always at 0,0)
    //                 /\
    //     dPos1      /  \    dPos2
    //     dColor1   /    \   dColor2
    //              / dPos \
    //             /--------\ (span)
    //            /  dColor  \
    //           /____________\
    //    comb corner 1     comb corner 2
    //
    // Pos1, Pos2, Color1, Color2 are running terms for both sides of the triangle
    // incremented by dPos1/2 and dColor1/2.
    // dPos and dColor are used to interpolate a span between the values just mentioned.
    //
    // The small combs are actually oriented with corner 0 at top (i.e. mirrored at y = x,
    // compared with the values in FCombCorners), we can achieve that by simply exchanging
    // X and Y values.
    Scale := 2 * FRadius * cos(Pi / 6);
    Pos1.X := FCombCorners[I].Y * Scale;
    Pos1.Y := FCombCorners[I].X * Scale;
    Color1 := DefColors[I];
    if I = 5 then
    begin
      Pos2.X := FCombCorners[0].Y * Scale;
      Pos2.Y := FCombCorners[0].X * Scale;
      Color2 := DefColors[0];
    end
    else
    begin
      Pos2.X := FCombCorners[I + 1].Y * Scale;
      Pos2.Y := FCombCorners[I + 1].X * Scale;
      Color2 := DefColors[I + 1];
    end;
    dPos1.X := Pos1.X / (FLevels - 1);
    dPos1.Y := Pos1.Y / (FLevels - 1);
    dPos2.X := Pos2.X / (FLevels - 1);
    dPos2.Y := Pos2.Y / (FLevels - 1);
    dColor1.Red := (Color1.Red - FCenterColor.Red) / (FLevels - 1);
    dColor1.Green := (Color1.Green - FCenterColor.Green) / (FLevels - 1);
    dColor1.Blue := (Color1.Blue - FCenterColor.Blue) / (FLevels - 1);

    dColor2.Red := (Color2.Red - FCenterColor.Red) / (FLevels - 1);
    dColor2.Green := (Color2.Green - FCenterColor.Green) / (FLevels - 1);
    dColor2.Blue := (Color2.Blue - FCenterColor.Blue) / (FLevels - 1);

    Pos1 := DefCenter;
    Pos2 := DefCenter;
    Color1 := FCenterColor;
    Color2 := FCenterColor;

    // Now that we have finished the initialization for this step we'll go
    // through a loop for each level to calculate the spans.
    // We can ignore level 0 (as this is the center we already have determined) as well
    // as the last step of each span (as this is the start value in the next triangle and will
    // be calculated there). We have, though, take them into the calculation of the running terms.
    for Level := 0 to FLevels - 1 do
    begin
      if Level > 0 then
      begin
        // initialize span values
        dPos.X := (Pos2.X - Pos1.X) / Level;
        dPos.Y := (Pos2.Y - Pos1.Y) / Level;
        dColor.Red := (Color2.Red - Color1.Red) / Level;
        dColor.Green := (Color2.Green - Color1.Green) / Level;
        dColor.Blue := (Color2.Blue - Color1.Blue) / Level;
        CurrentPos := Pos1;
        CurrentColor := Color1;
        for J := 0 to Level - 1 do
        begin
          // store current values in the array
          FColorCombs[CurrentIndex].Position.X := Round(CurrentPos.X);
          FColorCombs[CurrentIndex].Position.Y := Round(CurrentPos.Y);
          FColorCombs[CurrentIndex].Color := RGBFromFloat(CurrentColor);
          Inc(CurrentIndex);

          // advance in span
          CurrentPos.X := CurrentPos.X + dPos.X;
          CurrentPos.Y := CurrentPos.Y + dPos.Y;

          CurrentColor.Red := CurrentColor.Red + dColor.Red;
          CurrentColor.Green := CurrentColor.Green + dColor.Green;
          CurrentColor.Blue := CurrentColor.Blue + dColor.Blue;
        end;
      end;
      // advance running terms
      Pos1.X := Pos1.X + dPos1.X;
      Pos1.Y := Pos1.Y + dPos1.Y;
      Pos2.X := Pos2.X + dPos2.X;
      Pos2.Y := Pos2.Y + dPos2.Y;

      Color1.Red := Color1.Red + dColor1.Red;
      Color1.Green := Color1.Green + dColor1.Green;
      Color1.Blue := Color1.Blue + dColor1.Blue;

      Color2.Red := Color2.Red + dColor2.Red;
      Color2.Green := Color2.Green + dColor2.Green;
      Color2.Blue := Color2.Blue + dColor2.Blue;
    end;
  end;

  // second step is to build a list for the black & white area
  // 17 entries from pure white to pure black
  // the first and last are implicitely of double comb size
  SetLength(FBWCombs, 17);
  CurrentIndex := 0;
  FBWCombs[CurrentIndex].Color := GrayFromIntensity(255);
  FBWCombs[CurrentIndex].Position := Point(FCombSize, FCombSize);
  Inc(CurrentIndex);

  CurrentPos.X := 3 * FCombSize;
  CurrentPos.Y := 3 * (FCombSize div 4);
  dPos.X := Round(FCombSize * cos(Pi / 6) / 2);
  dPos.Y := Round(FCombSize * (1 + sin(Pi / 6)) / 2);
  for I := 0 to 14 do
  begin
    FBWCombs[CurrentIndex].Color := GrayFromIntensity((16 - CurrentIndex) * 15);
    if Odd(I) then
      FBWCombs[CurrentIndex].Position := Point(Round(CurrentPos.X + I * dPos.X), Round(CurrentPos.Y + dPos.Y))
    else
      FBWCombs[CurrentIndex].Position := Point(Round(CurrentPos.X + I * dPos.X), Round(CurrentPos.Y));
    Inc(CurrentIndex);
  end;
  FBWCombs[CurrentIndex].Color := 0;
  FBWCombs[CurrentIndex].Position := Point(Round(CurrentPos.X + 16 * dPos.X + FCombSize), FCombSize);
  EnumerateCombs;
end;

// determine hint message and out-of-hint rect
procedure THexaColorPicker.CMHintShow(
  var Message: {$IFDEF FPC}TLMessage{$ELSE}TMessage{$ENDIF} );
var
  Index: Integer;
  Colors: TCombArray;
  cp: TPoint;
begin
  Colors := nil;
  if (GetColorUnderCursor <> clNone) or PtInRect(FSliderRect, Point(mX, mY)) then
  with TCMHintShow(Message) do
  begin
    if not ShowHint then
      Message.Result := 1
    else
    begin
      with HintInfo^ do
      begin
        // show that we want a hint
        Result := 0;
        cp := CursorPos;
        ReshowTimeout := 0; //1;
        HideTimeout := 5000;
        HintInfo^.CursorRect := Rect(cp.X, cp.Y, cp.X+1, cp.Y+1);
        if PtInRect(FSliderRect, cp) and FSliderVisible then
        begin
          // in case of the intensity slider we show the current intensity
          HintStr := FIntensityText + Format(': %d%%', [Round(100 * FCenterIntensity)]);
          HintPos := ClientToScreen(Point(FSliderRect.Right, CursorPos.Y - 8));
        end
        else
        begin
          Index := -1;
          if PtInRect(FBWCombRect, Point(CursorPos.X, CursorPos.Y)) then
          begin
            // considering black&white area...
            if csLButtonDown in ControlState then
              Index := -(FCustomIndex + 1)
            else
              Index := FindBWArea(CursorPos.X, CursorPos.Y);
            Colors := FBWCombs;
          end
          else
          if PtInRect(FColorCombRect, Point(CursorPos.X, CursorPos.Y)) then
          begin
            // considering color comb area...
            if csLButtonDown in ControlState then
              Index := FCustomIndex - 1
            else
              Index := FindColorArea(CursorPos.X, CursorPos.Y);
            Colors := FColorCombs;
          end;
          if (Index > -1) and (Colors <> nil) then
            HintStr := FormatHint(FHintFormat, Colors[Index].Color);
        end;
      end;
    end;
  end;
end;

procedure THexaColorPicker.SetSelectedColor(Value: TColor);
begin
  FCurrentColor := Value;
  SelectColor(Value);
  Invalidate;
end;

procedure THexaColorPicker.EndSelection;
begin
  if FCustomIndex < 0 then
    SetSelectedColor(FBWCombs[-(FCustomIndex + 1)].Color)
  else
  if FCustomIndex > 0 then
    SetSelectedColor(FColorCombs[FCustomIndex - 1].Color)
  else
    SetSelectedColor(clNone);
end;

function THexaColorPicker.GetColorUnderCursor: TColor;
begin
  Result := FUnderCursor;
end;

function THexaColorPicker.GetColorAtPoint(X, Y: integer): TColor;
var
  Index: Integer;
  Colors: TCombArray;
begin
  Colors := nil;
  Index := -1;
  if PtInRect(FBWCombRect, Point(X, Y)) then
  begin
    Index := FindBWArea(X, Y);
    Colors := FBWCombs;
  end
  else
  if PtInRect(FColorCombRect, Point(X, Y)) then
  begin
    Index := FindColorArea(X, Y);
    Colors := FColorCombs;
  end;
  if (Index > -1) and (Colors <> nil) then
    Result := Colors[Index].Color
  else
    Result := clNone;
end;

function THexaColorPicker.GetHexColorUnderCursor: string;
begin
  Result := ColorToHex(GetColorUnderCursor);
end;

function THexaColorPicker.GetHexColorAtPoint(X, Y: integer): string;
begin
  Result := ColorToHex(GetColorAtPoint(X, Y));
end;

procedure THexaColorPicker.EnumerateCombs;
var
  i, k: integer;
begin
  k := 0;
  if FBWCombs <> nil then
    for i := 1 to High(FBWCombs) do
    begin
      case i of
      // b & w comb indices
        1: k := -1;
        2: k := -9;
        3: k := -2;
        4: k := -10;
        5: k := -3;
        6: k := -11;
        7: k := -4;
        8: k := -12;
        9: k := -5;
       10: k := -13;
       11: k := -6;
       12: k := -14;
       13: k := -7;
       14: k := -15;
       15: k := -8;
       // big black comb index (match center comb)
       16: K := 64;
      end;
      FBWCombs[i].TabIndex := k;
    end;
  if FColorCombs <> nil then
    for i := 0 to High(FColorCombs) do
    begin
      case i of
        // center comb index
        0: k := 64;
        // color comb indices
        1: k := 65;
        2: k := 66;
        3: k := 78;
        4: k := 67;
        5: k := 79;
        6: k := 90;
        7: k := 68;
        8: k := 80;
        9: k := 91;
       10: k := 101;
       11: k := 69;
       12: k := 81;
       13: k := 92;
       14: k := 102;
       15: k := 111;
       16: k := 70;
       17: k := 82;
       18: k := 93;
       19: k := 103;
       20: k := 112;
       21: k := 120;
       22: k := 77;
       23: k := 89;
       24: k := 88;
       25: k := 100;
       26: k := 99;
       27: k := 98;
       28: k := 110;
       29: k := 109;
       30: k := 108;
       31: k := 107;
       32: k := 119;
       33: k := 118;
       34: k := 117;
       35: k := 116;
       36: k := 115;
       37: k := 127;
       38: k := 126;
       39: k := 125;
       40: k := 124;
       41: k := 123;
       42: k := 122;
       43: k := 76;
       44: k := 87;
       45: k := 75;
       46: k := 97;
       47: k := 86;
       48: k := 74;
       49: k := 106;
       50: k := 96;
       51: k := 85;
       52: k := 73;
       53: k := 114;
       54: k := 105;
       55: k := 95;
       56: k := 84;
       57: k := 72;
       58: k := 121;
       59: k := 113;
       60: k := 104;
       61: k := 94;
       62: k := 83;
       63: k := 71;
       64: k := 63;
       65: k := 62;
       66: k := 50;
       67: k := 61;
       68: k := 49;
       69: k := 38;
       70: k := 60;
       71: k := 48;
       72: k := 37;
       73: k := 27;
       74: k := 59;
       75: k := 47;
       76: k := 36;
       77: k := 26;
       78: k := 17;
       79: k := 58;
       80: k := 46;
       81: k := 35;
       82: k := 25;
       83: k := 16;
       84: k := 8;
       85: k := 51;
       86: k := 39;
       87: k := 40;
       88: k := 28;
       89: k := 29;
       90: k := 30;
       91: k := 18;
       92: k := 19;
       93: k := 20;
       94: k := 21;
       95: k := 9;
       96: k := 10;
       97: k := 11;
       98: k := 12;
       99: k := 13;
      100: k := 1;
      101: k := 2;
      102: k := 3;
      103: k := 4;
      104: k := 5;
      105: k := 6;
      106: k := 52;
      107: k := 41;
      108: k := 53;
      109: k := 31;
      110: k := 42;
      111: k := 54;
      112: k := 22;
      113: k := 32;
      114: k := 43;
      115: k := 55;
      116: k := 14;
      117: k := 23;
      118: k := 33;
      119: k := 44;
      120: k := 56;
      121: k := 7;
      122: k := 15;
      123: k := 24;
      124: k := 34;
      125: k := 45;
      126: k := 57;
    end;
    FColorCombs[i].TabIndex := k;
  end;
end;

procedure THexaColorPicker.SelectCombIndex(i: integer);
var
  j: integer;
begin
  if i > 0 then
  begin
    if FColorCombs <> nil then
    for j := 0 to High(FColorCombs) do
    begin
      if FColorCombs[j].TabIndex = i then
      begin
        SetSelectedColor(FColorCombs[j].Color);
        Break;
      end;
    end;
  end
  else
  if FBWCombs <> nil then
    for j := 1 to High(FBWCombs) - 1 do
    begin
      if FBWCombs[j].TabIndex = i then
      begin
        SetSelectedColor(FBWCombs[j].Color);
        Break;
      end;
    end;
end;

procedure THexaColorPicker.Resize;
var
  rw, rh: integer;
begin
  if (Width >= 93) and (Height >= 85) then
  begin
    if FSliderVisible then
      rw := Round((Width - 10 - FSliderWidth)/2)
    else
      rw := Round(Width/2 - 5);
    rh := Round((24/53)*(Height - 6));
    SetRadius(Min(rw, rh));
  end;
  inherited;
end;

function THexaColorPicker.SelectAvailableColor(Color: TColor): boolean;
var
  I: integer;
  C: COLORREF;
  found: Boolean;
begin
  found := False;
  Result := false;
  C := ColorToRGB(Color);
  if FColorCombs = nil then CalculateCombLayout;
  FCustomIndex := 0;
  FSelectedIndex := NoCell;
  for I := 0 to High(FBWCombs) do
    if FBWCombs[I].Color = C then
    begin
      FSelectedIndex := CustomCell;
      FCustomIndex := -(I + 1);
      found := True;
      Result := true;
      Break;
    end;
  if not found then
    for I := 0 to High(FColorCombs) do
      if FColorCombs[I].Color = C then
      begin
        FSelectedIndex := CustomCell;
        FCustomIndex := I + 1;
        Result := true;
        Break;
      end;
end;

procedure THexaColorPicker.SelectColor(Color: TColor);
begin
  SelectAvailableColor(Color);
  Invalidate;
  if Assigned(OnChange) then OnChange(Self);
end;

procedure THexaColorPicker.SetIntensity(v: integer);
var
  R: TRect;
begin
  FCenterIntensity := EnsureRange(v/100, 0, 1);
  FCenterColor.Red := DefCenterColor.Red * FCenterIntensity;
  FCenterColor.Green := DefCenterColor.Green * FCenterIntensity;
  FCenterColor.Blue := DefCenterColor.Blue * FCenterIntensity;
  R := FSliderRect;
  Dec(R.Top, 3);
  Inc(R.Bottom, 3);
  Inc(R.Left, 10);
  InvalidateRect(Handle, @R, False);
  FColorCombs := nil;
  InvalidateRect(Handle, @FColorCombRect, False);
  InvalidateRect(Handle, @FCustomColorRect, False);
  CalculateCombLayout;
  EndSelection;
  if Assigned(FOnIntensityChange) then
    FOnIntensityChange(Self);
end;

procedure THexaColorPicker.SetMarker(Value: TMarker);
begin
  if FMarker <> Value then
  begin
    FMarker := Value;
    DrawAll;
    CalculateCombLayout;
    Invalidate;
  end;
end;

procedure THexaColorPicker.SetNewArrowStyle(Value: boolean);
begin
  if FNewArrowStyle <> Value then
  begin
    FNewArrowStyle := Value;
    DrawAll;
    CalculateCombLayout;
    Invalidate;
  end;
end;

procedure THexaColorPicker.SetRadius(r: integer);
begin
  if Parent = nil then
    exit;
  FRadius := r;
  DrawAll;
  CalculateCombLayout;
  Invalidate;
end;

procedure THexaColorPicker.SetSliderVisible(Value: boolean);
begin
  if FSliderVisible <> Value then
  begin
    FSliderVisible := Value;
    DrawAll;
    CalculateCombLayout;
    Invalidate;
  end;
end;

procedure THexaColorPicker.SetSliderWidth(w: integer);
begin
  if (FSliderWidth <> w) and FSliderVisible then
  begin
    FSliderWidth := w;
    DrawAll;
    Width := FSliderRect.Right + 2;
    CalculateCombLayout;
    Invalidate;
  end;
end;

procedure THexaColorPicker.WMLButtonDown(
  var Message: {$IFDEF FPC}TLMLButtonDown{$ELSE}TWMLButtonDown{$ENDIF} );
begin
  inherited;
  SetFocus; // needed so the key events work
  if PtInRect(ClientRect, Point(Message.XPos, Message.YPos)) then
    HandleCustomColors(Message);
end;

procedure THexaColorPicker.WMLButtonUp(
  var Message: {$IFDEF FPC}TLMLButtonUp{$ELSE}TWMLButtonUp{$ENDIF} );
var
  LastMode: TSelectionMode;
begin
  inherited;
  LastMode := FSelectionMode;
  FSelectionMode := smNone;
  if (FSelectedIndex = CustomCell) and (FCustomIndex <> 0) then
  begin
    if ((FSelectedIndex = CustomCell) and (LastMode in [smColor, smBW])) or
       (FSelectedIndex <> NoCell) and (FSelectedIndex <> CustomCell)
    then
      EndSelection
  end;
end;

procedure THexaColorPicker.WMMouseMove(
  var Message: {$IFDEF FPC}TLMMouseMove{$ELSE}TWMMouseMove{$ENDIF} );
var
  Shift: TShiftState;
  Index: Integer;
  Colors: TCombArray;
begin
  inherited;
  mX := Message.XPos;
  mY := Message.YPos;
  //get color under cursor
  Colors := nil;
  FUnderCursor := clNone;
  if PtInRect(FBWCombRect, Point(Message.XPos, Message.YPos)) then
  begin
    Index := FindBWArea(Message.XPos, Message.YPos);
    Colors := FBWCombs;
    if (Index > -1) and (Colors <> nil) then
      FUnderCursor := Colors[Index].Color;
  end
  else
  if PtInRect(FColorCombRect, Point(Message.XPos, Message.YPos)) then
  begin
    Index := FindColorArea(Message.XPos, Message.YPos);
    Colors := FColorCombs;
    if (Index > -1) and (Colors <> nil) then
      FUnderCursor := Colors[Index].Color;
   end
   else
     FUnderCursor := clNone;
  // further process message
  Shift := KeysToShiftState(Message.Keys);
  if ssLeft in Shift then
    HandleCustomColors(Message);
end;

end.