{
  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 FlatMemo;

{
  Read-only/editable memo с полностью управляемым оформлением и тонким
  overlay-scrollbar. Внутри остаётся обычный TMemo, поэтому выделение,
  клавиатура и копирование работают штатно на всех LCL widgetset.
}

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Math, Types,
  Forms, Controls, Graphics, StdCtrls, ExtCtrls,
  AppTheme, DpiUtils, OverlayScrollBar;

type
  TFlatMemo = class(TCustomControl)
  private
    FMemo: TMemo;
    FScrollHost: TPanel;
    FScrollBar: TOverlayScrollBar;
    FTheme: TAppTheme;
    FSyncing: Boolean;
    FOnChange: TNotifyEvent;
    function GetLines: TStrings;
    function GetReadOnly: Boolean;
    procedure SetReadOnly(AValue: Boolean);
    function GetWordWrap: Boolean;
    procedure SetWordWrap(AValue: Boolean);
    function GetSelStart: Integer;
    procedure SetSelStart(AValue: Integer);
    function GetText: string;
    procedure SetText(const AValue: string);
    procedure MemoChange(Sender: TObject);
    procedure MemoKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure MemoMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure MemoMouseWheel(Sender: TObject; Shift: TShiftState;
      WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
    procedure ScrollChanged(Sender: TObject);
    procedure UpdateScrollBar;
    procedure LayoutChildren;
  protected
    procedure Paint; override;
    procedure Resize; override;
    procedure FontChanged(Sender: TObject); override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure Append(const AValue: string);
    procedure SetAppTheme(const T: TAppTheme);
    procedure SyncScrollBar;

    property Lines: TStrings read GetLines;
    property ReadOnly: Boolean read GetReadOnly write SetReadOnly;
    property WordWrap: Boolean read GetWordWrap write SetWordWrap;
    property SelStart: Integer read GetSelStart write SetSelStart;
    property Text: string read GetText write SetText;
    property Align;
    property Anchors;
    property BorderSpacing;
    property Color;
    property Font;
    property Enabled;
    property TabOrder;
    property TabStop;
    property Visible;
    // Правка текста пользователем (проброс OnChange внутреннего TMemo).
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  end;

implementation

constructor TFlatMemo.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := ControlStyle + [csOpaque];
  Width := DpiScale(300);
  Height := DpiScale(160);
  FTheme := DarkTheme;
  Color := FTheme.BG;
  TabStop := False;

  FMemo := TMemo.Create(Self);
  FMemo.Parent := Self;
  FMemo.Align := alClient;
  FMemo.BorderSpacing.Around := 1;
  FMemo.BorderStyle := bsNone;
  FMemo.ScrollBars := ssVertical;
  FMemo.WordWrap := False;
  FMemo.ParentFont := True;
  FMemo.Color := FTheme.BG;
  FMemo.Font.Color := FTheme.Text;
  FMemo.OnChange := @MemoChange;
  FMemo.OnKeyUp := @MemoKeyUp;
  FMemo.OnMouseUp := @MemoMouseUp;
  FMemo.OnMouseWheel := @MemoMouseWheel;

  // TPaintBox сам не может перекрыть оконный TMemo, поэтому scrollbar живёт
  // внутри отдельного оконного TPanel, поднятого над нативной полосой.
  FScrollHost := TPanel.Create(Self);
  FScrollHost.Parent := Self;
  FScrollHost.BevelOuter := bvNone;
  FScrollHost.Color := FTheme.BG;
  FScrollHost.OnMouseWheel := @MemoMouseWheel;

  FScrollBar := TOverlayScrollBar.Create(Self);
  FScrollBar.Parent := FScrollHost;
  FScrollBar.Align := alClient;
  FScrollBar.HoverTarget := Self;
  FScrollBar.OnMouseWheel := @MemoMouseWheel;
  FScrollBar.OnPositionChange := @ScrollChanged;

  LayoutChildren;
  SetAppTheme(FTheme);
end;

procedure TFlatMemo.Paint;
begin
  Canvas.Brush.Style := bsSolid;
  Canvas.Brush.Color := FTheme.Border;
  Canvas.FillRect(ClientRect);
end;

procedure TFlatMemo.Resize;
begin
  inherited Resize;
  LayoutChildren;
  UpdateScrollBar;
end;

procedure TFlatMemo.FontChanged(Sender: TObject);
begin
  inherited FontChanged(Sender);
  if FMemo <> nil then
  begin
    FMemo.Font.Assign(Font);
    UpdateScrollBar;
  end;
end;

procedure TFlatMemo.LayoutChildren;
var
  GutterW: Integer;
begin
  if (FMemo = nil) or (FScrollHost = nil) then Exit;
  GutterW := Max(DpiScale(8), FMemo.VertScrollBar.Size);
  FScrollHost.SetBounds(Max(1, ClientWidth - GutterW - 1), 1,
    GutterW, Max(0, ClientHeight - 2));
  FScrollHost.BringToFront;
end;

procedure TFlatMemo.UpdateScrollBar;
var
  MaxPos, Viewport: Integer;
begin
  if FSyncing or (FMemo = nil) or (FScrollBar = nil) then Exit;
  FSyncing := True;
  try
    MaxPos := Max(0, FMemo.VertScrollBar.Range - FMemo.VertScrollBar.Page);
    Viewport := Max(1, FMemo.VertScrollBar.Page);
    FScrollBar.SetRange(MaxPos, Viewport);
    FScrollBar.Position := EnsureRange(FMemo.VertScrollBar.Position, 0, MaxPos);
    LayoutChildren;
  finally
    FSyncing := False;
  end;
end;

procedure TFlatMemo.SyncScrollBar;
begin
  UpdateScrollBar;
end;

procedure TFlatMemo.ScrollChanged(Sender: TObject);
begin
  if FSyncing or (FMemo = nil) or (FScrollBar = nil) then Exit;
  FMemo.VertScrollBar.Position := FScrollBar.Position;
end;

procedure TFlatMemo.MemoMouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  Step: Integer;
begin
  UpdateScrollBar;
  if (FScrollBar = nil) or (FScrollBar.Maximum <= 0) then Exit;
  Step := Max(1, FMemo.VertScrollBar.Increment * 3);
  if WheelDelta > 0 then FScrollBar.ScrollBy(-Step)
  else if WheelDelta < 0 then FScrollBar.ScrollBy(Step);
  Handled := WheelDelta <> 0;
end;

procedure TFlatMemo.MemoChange(Sender: TObject);
begin
  UpdateScrollBar;
  if Assigned(FOnChange) then FOnChange(Self);
end;

procedure TFlatMemo.MemoKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  UpdateScrollBar;
end;

procedure TFlatMemo.MemoMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  UpdateScrollBar;
end;

function TFlatMemo.GetLines: TStrings;
begin
  Result := FMemo.Lines;
end;

function TFlatMemo.GetReadOnly: Boolean;
begin
  Result := FMemo.ReadOnly;
end;

procedure TFlatMemo.SetReadOnly(AValue: Boolean);
begin
  FMemo.ReadOnly := AValue;
end;

function TFlatMemo.GetWordWrap: Boolean;
begin
  Result := FMemo.WordWrap;
end;

procedure TFlatMemo.SetWordWrap(AValue: Boolean);
begin
  FMemo.WordWrap := AValue;
  UpdateScrollBar;
end;

function TFlatMemo.GetSelStart: Integer;
begin
  Result := FMemo.SelStart;
end;

procedure TFlatMemo.SetSelStart(AValue: Integer);
begin
  FMemo.SelStart := AValue;
  UpdateScrollBar;
end;

function TFlatMemo.GetText: string;
begin
  Result := FMemo.Text;
end;

procedure TFlatMemo.SetText(const AValue: string);
begin
  FMemo.Text := AValue;
  UpdateScrollBar;
end;

procedure TFlatMemo.Append(const AValue: string);
begin
  FMemo.Append(AValue);
  UpdateScrollBar;
end;

procedure TFlatMemo.SetAppTheme(const T: TAppTheme);
begin
  FTheme := T;
  Color := T.BG;
  if FMemo <> nil then
  begin
    FMemo.Color := T.BG;
    FMemo.Font.Color := T.Text;
  end;
  if FScrollHost <> nil then FScrollHost.Color := T.BG;
  if FScrollBar <> nil then
    FScrollBar.SetColors(T.BG, T.Border,
      T.SliderThumbHot, T.SliderThumbDrag);
  Invalidate;
end;

end.
