Files
ewsdr/FlatEdit.pas
T
2026-05-25 14:43:05 +03:00

587 lines
13 KiB
ObjectPascal

unit FlatEdit;
{ Custom single-line edit with canvas rendering.
Use instead of TEdit where native widgetset styling is undesirable. }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Controls, Graphics, Forms, LCLType, LMessages, Types,
Clipbrd, Math, LazUTF8, AppTheme;
type
TFlatEdit = class(TCustomControl)
private
FText: string;
FTextHint: string;
FPasswordChar: Char;
FMaxLength: Integer;
FCaretPos: Integer;
FSelStart: Integer;
FSelLen: Integer;
FHot: Boolean;
FEditing: Boolean;
FDragging: Boolean;
FOnChange: TNotifyEvent;
FClrOuterBG: TColor;
FClrBG: TColor;
FClrBGHot: TColor;
FClrBorder: TColor;
FClrText: TColor;
FClrTextDim: TColor;
FClrSelBG: TColor;
FClrSelText: TColor;
function DpiScale(V: Integer): Integer;
function DisplayText: string;
function TextLen: Integer;
function HasSelection: Boolean;
function SelFirst: Integer;
function SelAfter: Integer;
function CaretPosAtX(X: Integer): Integer;
procedure SetText(const V: string);
procedure SetTextHint(const V: string);
procedure SetPasswordChar(V: Char);
procedure SetMaxLength(V: Integer);
procedure SetCaretPos(V: Integer);
procedure DeleteSelection;
procedure InsertText(const S: string);
procedure DoChange;
procedure AppUserInput(Sender: TObject; Msg: Cardinal);
procedure CMTextChanged(var Msg: TLMessage); message CM_TEXTCHANGED;
protected
procedure Paint; override;
procedure MouseEnter; override;
procedure MouseLeave; 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;
procedure DblClick; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure KeyPress(var Key: char); override;
procedure UTF8KeyPress(var UTF8Key: TUTF8Char); override;
procedure DoEnter; override;
procedure DoExit; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure SetAppTheme(const T: TAppTheme);
procedure SelectAll;
property Text: string read FText write SetText;
property TextHint: string read FTextHint write SetTextHint;
property PasswordChar: Char read FPasswordChar write SetPasswordChar;
property MaxLength: Integer read FMaxLength write SetMaxLength;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property Align;
property Anchors;
property Enabled;
property Font;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property TabOrder;
property TabStop;
property Tag;
property Visible;
property OnEnter;
property OnExit;
property OnKeyDown;
property OnKeyPress;
end;
implementation
const
BASE_PAD_X = 8;
constructor TFlatEdit.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque, csCaptureMouse, csClickEvents];
Width := 120;
Height := DpiScale(26);
TabStop := True;
Cursor := crIBeam;
FMaxLength := 0;
FCaretPos := 0;
FSelStart := 0;
FSelLen := 0;
FEditing := False;
FDragging := False;
Font.Name := 'Courier New';
Font.Size := 9;
FClrOuterBG := TColor($00181818);
FClrBG := TColor($001A1A1A);
FClrBGHot := TColor($00202020);
FClrBorder := TColor($00444444);
FClrText := TColor($00E0E0E0);
FClrTextDim := TColor($00666666);
FClrSelBG := TColor($000C3010);
FClrSelText := TColor($0000FF88);
Application.AddOnUserInputHandler(@AppUserInput);
end;
destructor TFlatEdit.Destroy;
begin
Application.RemoveOnUserInputHandler(@AppUserInput);
inherited Destroy;
end;
function TFlatEdit.DpiScale(V: Integer): Integer;
begin
Result := MulDiv(V, Screen.PixelsPerInch, 96);
if (V > 0) and (Result < 1) then
Result := 1;
end;
procedure TFlatEdit.SetAppTheme(const T: TAppTheme);
begin
FClrOuterBG := T.Panel;
FClrBG := T.BtnNorm;
FClrBGHot := T.BtnHot;
FClrBorder := T.Border;
FClrText := T.Text;
FClrTextDim := T.TextDim;
FClrSelBG := T.BtnActive;
FClrSelText := T.BtnTextActive;
Font.Color := T.Text;
Invalidate;
end;
function TFlatEdit.DisplayText: string;
begin
if FPasswordChar <> #0 then
Result := StringOfChar(FPasswordChar, TextLen)
else
Result := FText;
end;
function TFlatEdit.TextLen: Integer;
begin
Result := UTF8Length(FText);
end;
function TFlatEdit.HasSelection: Boolean;
begin
Result := FSelLen <> 0;
end;
function TFlatEdit.SelFirst: Integer;
begin
if FSelLen >= 0 then
Result := FSelStart
else
Result := FSelStart + FSelLen;
end;
function TFlatEdit.SelAfter: Integer;
begin
if FSelLen >= 0 then
Result := FSelStart + FSelLen
else
Result := FSelStart;
end;
function TFlatEdit.CaretPosAtX(X: Integer): Integer;
var
I, Pad, RelX, BestDist, Dist: Integer;
S: string;
begin
Pad := DpiScale(BASE_PAD_X);
RelX := X - Pad;
S := DisplayText;
Result := 0;
BestDist := Abs(RelX);
for I := 1 to UTF8Length(S) do
begin
Dist := Abs(RelX - Canvas.TextWidth(UTF8Copy(S, 1, I)));
if Dist < BestDist then
begin
BestDist := Dist;
Result := I;
end;
end;
end;
procedure TFlatEdit.SetText(const V: string);
var
S: string;
begin
S := V;
if (FMaxLength > 0) and (UTF8Length(S) > FMaxLength) then
S := UTF8Copy(S, 1, FMaxLength);
if FText = S then Exit;
FText := S;
FCaretPos := EnsureRange(FCaretPos, 0, TextLen);
FSelStart := FCaretPos;
FSelLen := 0;
Invalidate;
DoChange;
end;
procedure TFlatEdit.SetTextHint(const V: string);
begin
if FTextHint = V then Exit;
FTextHint := V;
Invalidate;
end;
procedure TFlatEdit.SetPasswordChar(V: Char);
begin
if FPasswordChar = V then Exit;
FPasswordChar := V;
Invalidate;
end;
procedure TFlatEdit.SetMaxLength(V: Integer);
begin
FMaxLength := Max(0, V);
if (FMaxLength > 0) and (UTF8Length(FText) > FMaxLength) then
SetText(UTF8Copy(FText, 1, FMaxLength));
end;
procedure TFlatEdit.SetCaretPos(V: Integer);
begin
FCaretPos := EnsureRange(V, 0, TextLen);
FSelStart := FCaretPos;
FSelLen := 0;
Invalidate;
end;
procedure TFlatEdit.DeleteSelection;
var
A, B: Integer;
begin
if not HasSelection then Exit;
A := SelFirst;
B := SelAfter;
UTF8Delete(FText, A + 1, B - A);
FCaretPos := A;
FSelStart := FCaretPos;
FSelLen := 0;
end;
procedure TFlatEdit.InsertText(const S: string);
var
AddText: string;
SpaceLeft: Integer;
begin
if S = '' then Exit;
DeleteSelection;
AddText := S;
if FMaxLength > 0 then
begin
SpaceLeft := FMaxLength - TextLen;
if SpaceLeft <= 0 then Exit;
AddText := UTF8Copy(AddText, 1, SpaceLeft);
end;
UTF8Insert(AddText, FText, FCaretPos + 1);
Inc(FCaretPos, UTF8Length(AddText));
FSelStart := FCaretPos;
FSelLen := 0;
Invalidate;
DoChange;
end;
procedure TFlatEdit.DoChange;
begin
if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TFlatEdit.AppUserInput(Sender: TObject; Msg: Cardinal);
var
P: TPoint;
begin
if not FEditing then Exit;
case Msg of
LM_LBUTTONDOWN, LM_RBUTTONDOWN, LM_MBUTTONDOWN: ;
else
Exit;
end;
P := ScreenToClient(Mouse.CursorPos);
if PtInRect(ClientRect, P) then Exit;
FDragging := False;
MouseCapture := False;
FEditing := False;
FHot := False;
FSelLen := 0;
Invalidate;
end;
procedure TFlatEdit.CMTextChanged(var Msg: TLMessage);
begin
Invalidate;
end;
procedure TFlatEdit.Paint;
var
R, TextR, SelR: TRect;
Pad, TextY, CaretX, A, B: Integer;
S, BeforeSel, SelText: string;
begin
R := ClientRect;
Canvas.Brush.Color := FClrOuterBG;
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Style := psClear;
Canvas.FillRect(R);
if FHot or FEditing then
Canvas.Brush.Color := FClrBGHot
else
Canvas.Brush.Color := FClrBG;
Canvas.Pen.Color := FClrBorder;
Canvas.Pen.Style := psSolid;
Canvas.Pen.Width := 1;
Canvas.Rectangle(R.Left, R.Top, R.Right, R.Bottom);
Canvas.Font.Assign(Font);
Pad := DpiScale(BASE_PAD_X);
TextR := Rect(Pad, 0, Width - Pad, Height);
TextY := (Height - Canvas.TextHeight('Ag')) div 2;
if (FText = '') and not FEditing and (FTextHint <> '') then
begin
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FClrTextDim;
Canvas.TextRect(TextR, TextR.Left, TextY, FTextHint);
Exit;
end;
S := DisplayText;
if HasSelection then
begin
A := SelFirst;
B := SelAfter;
BeforeSel := UTF8Copy(S, 1, A);
SelText := UTF8Copy(S, A + 1, B - A);
SelR := Rect(TextR.Left + Canvas.TextWidth(BeforeSel), TextY,
TextR.Left + Canvas.TextWidth(BeforeSel + SelText), TextY + Canvas.TextHeight('Ag'));
Canvas.Brush.Color := FClrSelBG;
Canvas.Brush.Style := bsSolid;
Canvas.Pen.Style := psClear;
Canvas.FillRect(SelR);
end;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FClrText;
Canvas.TextRect(TextR, TextR.Left, TextY, S);
if HasSelection then
begin
A := SelFirst;
B := SelAfter;
BeforeSel := UTF8Copy(S, 1, A);
SelText := UTF8Copy(S, A + 1, B - A);
Canvas.Font.Color := FClrSelText;
Canvas.TextOut(TextR.Left + Canvas.TextWidth(BeforeSel), TextY, SelText);
end;
if FEditing and not HasSelection then
begin
CaretX := TextR.Left + Canvas.TextWidth(UTF8Copy(S, 1, FCaretPos));
Canvas.Pen.Color := FClrText;
Canvas.Pen.Style := psSolid;
Canvas.Pen.Width := 1;
Canvas.Line(CaretX, TextY, CaretX, TextY + Canvas.TextHeight('Ag'));
end;
end;
procedure TFlatEdit.MouseEnter;
begin
inherited;
FHot := True;
Invalidate;
end;
procedure TFlatEdit.MouseLeave;
begin
inherited;
FHot := False;
Invalidate;
end;
procedure TFlatEdit.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
Pos: Integer;
begin
inherited;
if (Button <> mbLeft) or not Enabled then Exit;
SetFocus;
FEditing := True;
FDragging := True;
MouseCapture := True;
Pos := CaretPosAtX(X);
FCaretPos := Pos;
FSelStart := Pos;
FSelLen := 0;
Invalidate;
end;
procedure TFlatEdit.MouseMove(Shift: TShiftState; X, Y: Integer);
var
Pos: Integer;
begin
inherited;
if not FDragging then Exit;
Pos := CaretPosAtX(X);
FCaretPos := Pos;
FSelLen := Pos - FSelStart;
Invalidate;
end;
procedure TFlatEdit.MouseUp(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
begin
inherited;
if Button <> mbLeft then Exit;
FDragging := False;
MouseCapture := False;
Invalidate;
end;
procedure TFlatEdit.DblClick;
begin
inherited;
FEditing := True;
SelectAll;
end;
procedure TFlatEdit.KeyDown(var Key: Word; Shift: TShiftState);
begin
inherited;
if not Enabled then Exit;
FEditing := True;
case Key of
VK_LEFT:
begin
SetCaretPos(FCaretPos - 1);
Key := 0;
end;
VK_RIGHT:
begin
SetCaretPos(FCaretPos + 1);
Key := 0;
end;
VK_HOME:
begin
SetCaretPos(0);
Key := 0;
end;
VK_END:
begin
SetCaretPos(TextLen);
Key := 0;
end;
VK_BACK:
begin
if HasSelection then
DeleteSelection
else if FCaretPos > 0 then
begin
UTF8Delete(FText, FCaretPos, 1);
Dec(FCaretPos);
end;
FSelStart := FCaretPos;
FSelLen := 0;
Invalidate;
DoChange;
Key := 0;
end;
VK_DELETE:
begin
if HasSelection then
DeleteSelection
else if FCaretPos < TextLen then
UTF8Delete(FText, FCaretPos + 1, 1);
FSelStart := FCaretPos;
FSelLen := 0;
Invalidate;
DoChange;
Key := 0;
end;
Ord('A'):
if ssCtrl in Shift then
begin
SelectAll;
Key := 0;
end;
Ord('C'):
if (ssCtrl in Shift) and HasSelection then
begin
Clipboard.AsText := UTF8Copy(FText, SelFirst + 1, SelAfter - SelFirst);
Key := 0;
end;
Ord('X'):
if (ssCtrl in Shift) and HasSelection then
begin
Clipboard.AsText := UTF8Copy(FText, SelFirst + 1, SelAfter - SelFirst);
DeleteSelection;
Invalidate;
DoChange;
Key := 0;
end;
Ord('V'):
if ssCtrl in Shift then
begin
InsertText(Clipboard.AsText);
Key := 0;
end;
end;
end;
procedure TFlatEdit.KeyPress(var Key: char);
begin
inherited;
if not Enabled then Exit;
if Key >= #32 then
Key := #0;
end;
procedure TFlatEdit.UTF8KeyPress(var UTF8Key: TUTF8Char);
begin
inherited;
if not Enabled then Exit;
FEditing := True;
if (UTF8Key <> '') and (UTF8Key[1] >= #32) then
begin
InsertText(UTF8Key);
UTF8Key := '';
end;
end;
procedure TFlatEdit.DoEnter;
begin
inherited;
FEditing := True;
Invalidate;
end;
procedure TFlatEdit.DoExit;
begin
inherited;
FEditing := False;
FHot := False;
FSelLen := 0;
Invalidate;
end;
procedure TFlatEdit.SelectAll;
begin
FSelStart := 0;
FSelLen := TextLen;
FCaretPos := TextLen;
Invalidate;
end;
end.