{ Copyright (C) 2026 - Uladzimir Karpenka, EW8BAK This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. } unit OverlayScrollBar; { Тонкий вертикальный overlay-scrollbar без системного оформления. Сам контрол всегда занимает зарезервированный gutter, но трек и бегунок рисуются только при Max > 0 и наведении мыши на HoverTarget. Содержимое контрол не двигает: владелец подписывается на OnPositionChange. } {$mode objfpc}{$H+} interface uses Classes, Controls, ExtCtrls, Forms, Graphics, Math, Types, LCLType; type TOverlayScrollBar = class(TPaintBox) private FHoverTarget: TControl; FMaximum: Integer; FPosition: Integer; FViewportSize: Integer; FHover: Boolean; FDragging: Boolean; FDragY: Integer; FDragPosition: Integer; FBGColor: TColor; FTrackColor: TColor; FThumbColor: TColor; FThumbDragColor: TColor; FOnPositionChange: TNotifyEvent; procedure AppUserInput(Sender: TObject; Msg: Cardinal); procedure SetPosition(AValue: Integer); function CursorOverTarget: Boolean; function ThumbRect: TRect; protected procedure Paint; override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure MouseMove(Shift: TShiftState; X, Y: Integer); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure SetRange(AMaximum, AViewportSize: Integer); procedure ScrollBy(ADelta: Integer); procedure SetColors(ABG, ATrack, AThumb, AThumbDrag: TColor); property HoverTarget: TControl read FHoverTarget write FHoverTarget; property Maximum: Integer read FMaximum; property Position: Integer read FPosition write SetPosition; property OnPositionChange: TNotifyEvent read FOnPositionChange write FOnPositionChange; end; implementation constructor TOverlayScrollBar.Create(AOwner: TComponent); begin inherited Create(AOwner); Width := 8; FMaximum := 0; FPosition := 0; FViewportSize := 0; FHover := False; FDragging := False; FBGColor := TColor($00181818); FTrackColor := TColor($00303030); FThumbColor := TColor($00407040); FThumbDragColor := TColor($00509050); Application.AddOnUserInputHandler(@AppUserInput); end; destructor TOverlayScrollBar.Destroy; begin Application.RemoveOnUserInputHandler(@AppUserInput); if GetCaptureControl = Self then SetCaptureControl(nil); inherited Destroy; end; procedure TOverlayScrollBar.SetColors(ABG, ATrack, AThumb, AThumbDrag: TColor); begin FBGColor := ABG; FTrackColor := ATrack; FThumbColor := AThumb; FThumbDragColor := AThumbDrag; Invalidate; end; procedure TOverlayScrollBar.SetRange(AMaximum, AViewportSize: Integer); var OldMaximum: Integer; begin OldMaximum := FMaximum; FMaximum := Max(0, AMaximum); FViewportSize := Max(0, AViewportSize); if FPosition > FMaximum then SetPosition(FMaximum) else if OldMaximum <> FMaximum then Invalidate; end; procedure TOverlayScrollBar.SetPosition(AValue: Integer); var NewValue: Integer; begin NewValue := EnsureRange(AValue, 0, FMaximum); if NewValue = FPosition then Exit; FPosition := NewValue; if Assigned(FOnPositionChange) then FOnPositionChange(Self); Invalidate; end; procedure TOverlayScrollBar.ScrollBy(ADelta: Integer); begin SetPosition(FPosition + ADelta); end; function TOverlayScrollBar.CursorOverTarget: Boolean; var P: TPoint; begin Result := False; if (FHoverTarget = nil) or (not FHoverTarget.Visible) then Exit; P := FHoverTarget.ScreenToClient(Mouse.CursorPos); Result := (P.X >= 0) and (P.Y >= 0) and (P.X < FHoverTarget.Width) and (P.Y < FHoverTarget.Height); end; procedure TOverlayScrollBar.AppUserInput(Sender: TObject; Msg: Cardinal); var NewHover: Boolean; begin NewHover := CursorOverTarget; if NewHover = FHover then Exit; FHover := NewHover; Invalidate; end; function TOverlayScrollBar.ThumbRect: TRect; var TrackTop, TrackH, ThumbH, Travel, ContentH: Integer; begin Result := Rect(0, 0, 0, 0); if (FMaximum <= 0) or (FViewportSize <= 0) then Exit; TrackTop := Scale96ToForm(4); TrackH := Height - 2 * TrackTop; if TrackH <= 0 then Exit; ContentH := FViewportSize + FMaximum; ThumbH := Max(Scale96ToForm(28), MulDiv(TrackH, FViewportSize, Max(1, ContentH))); ThumbH := Min(ThumbH, TrackH); Travel := TrackH - ThumbH; Result.Left := Max(1, (Width - Scale96ToForm(4)) div 2); Result.Right := Width - Result.Left; Result.Top := TrackTop; if (Travel > 0) and (FMaximum > 0) then Inc(Result.Top, MulDiv(Travel, FPosition, FMaximum)); Result.Bottom := Result.Top + ThumbH; end; procedure TOverlayScrollBar.Paint; var R: TRect; TrackX: Integer; begin Canvas.Brush.Style := bsSolid; Canvas.Brush.Color := FBGColor; Canvas.FillRect(ClientRect); if (FMaximum <= 0) or ((not FHover) and (not FDragging)) then Exit; TrackX := Width div 2; Canvas.Pen.Color := FTrackColor; Canvas.Pen.Width := Max(1, Scale96ToForm(1)); Canvas.Line(TrackX, Scale96ToForm(4), TrackX, Height - Scale96ToForm(4)); R := ThumbRect; if IsRectEmpty(R) then Exit; if FDragging then Canvas.Brush.Color := FThumbDragColor else Canvas.Brush.Color := FThumbColor; Canvas.Pen.Style := psClear; Canvas.RoundRect(R.Left, R.Top, R.Right, R.Bottom, Scale96ToForm(3), Scale96ToForm(3)); Canvas.Pen.Style := psSolid; end; procedure TOverlayScrollBar.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var R: TRect; begin inherited MouseDown(Button, Shift, X, Y); if (Button <> mbLeft) or (FMaximum <= 0) then Exit; R := ThumbRect; if PtInRect(R, Point(X, Y)) then begin FDragging := True; FDragY := Y; FDragPosition := FPosition; SetCaptureControl(Self); end else if Y < R.Top then ScrollBy(-FViewportSize + Scale96ToForm(40)) else ScrollBy(FViewportSize - Scale96ToForm(40)); Invalidate; end; procedure TOverlayScrollBar.MouseMove(Shift: TShiftState; X, Y: Integer); var R: TRect; TrackH, Travel: Integer; begin inherited MouseMove(Shift, X, Y); if not FDragging then Exit; R := ThumbRect; TrackH := Height - 2 * Scale96ToForm(4); Travel := TrackH - (R.Bottom - R.Top); if Travel <= 0 then Exit; SetPosition(FDragPosition + MulDiv(Y - FDragY, FMaximum, Travel)); end; procedure TOverlayScrollBar.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseUp(Button, Shift, X, Y); if Button <> mbLeft then Exit; FDragging := False; if GetCaptureControl = Self then SetCaptureControl(nil); Invalidate; end; end.