wlgui_widget вроде как и был. Давай все 4 файла. Спасибо!
Полная версия: wlgui_widget.pas + Label + CheckBox + Edit

Даю все 4 файла полностью. Заменяешь wlgui_widget.pas целиком, остальные — создаёшь.
1. wlgui_widget.pas (полный)
pascal

unit wlgui_widget;
{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  wlgui_types, wlgui_canvas, wlgui_font, wlgui_events;

type
  TWLWidget = class;
  TWLWidgetList = array of TWLWidget;

  { Базовый класс виджета с деревом, фокусом и диспетчеризацией. }
  TWLWidget = class
  private
    FRect: TRectI;
    FParent: TWLWidget;
    FChildren: TWLWidgetList;
    FVisible: Boolean;
    FEnabled: Boolean;
    FCanFocus: Boolean;
    FFocused: Boolean;
    FName: String;
    FFont: TWLFont;
    FTag: PtrInt;
    function GetChild(Index: Integer): TWLWidget;
    function GetChildCount: Integer;
    function GetRoot: TWLWidget;
  protected
    { Переопределяются в наследниках. }
    procedure DoPaint(ACanvas: TWLCanvas); virtual;
    function  DoMouseDown(X, Y: Integer): Boolean; virtual;
    function  DoMouseUp(X, Y: Integer): Boolean; virtual;
    procedure DoMouseMove(X, Y: Integer); virtual;
    procedure DoKeyDown(const E: TWLKeyEvent); virtual;
    procedure DoKeyUp(const E: TWLKeyEvent); virtual;
    procedure DoGotFocus; virtual;
    procedure DoLostFocus; virtual;
  public
    constructor Create(const ARect: TRectI); virtual;
    destructor Destroy; override;

    { Дерево }
    procedure AddChild(AChild: TWLWidget);
    procedure RemoveChild(AChild: TWLWidget);
    procedure ClearChildren;

    { Геометрия }
    function ScreenRect: TRectI;
    function ClientRect: TRectI;
    function HitTest(X, Y: Integer): TWLWidget;

    { Рисование }
    procedure Paint(ACanvas: TWLCanvas);

    { Диспетчеризация мыши }
    function DispatchMouseDown(X, Y: Integer): Boolean;
    function DispatchMouseUp(X, Y: Integer): Boolean;
    procedure DispatchMouseMove(X, Y: Integer);

    { Диспетчеризация клавиатуры — только focused виджету }
    function DispatchKeyDown(const E: TWLKeyEvent): Boolean;
    function DispatchKeyUp(const E: TWLKeyEvent): Boolean;

    { Фокус }
    procedure SetFocus;
    procedure ClearFocus;
    function GetFocusedWidget: TWLWidget;   // ищет в дереве от корня
    function CanFocus: Boolean; virtual;

    { Свойства }
    property Rect: TRectI read FRect write FRect;
    property Parent: TWLWidget read FParent;
    property Children[Index: Integer]: TWLWidget read GetChild;
    property ChildCount: Integer read GetChildCount;
    property Visible: Boolean read FVisible write FVisible;
    property Enabled: Boolean read FEnabled write FEnabled;
    property Focused: Boolean read FFocused;
    property Name: String read FName write FName;
    property Font: TWLFont read FFont write FFont;
    property Tag: PtrInt read FTag write FTag;
    property Root: TWLWidget read GetRoot;
  end;

implementation

{ ============================================================ }
{  TWLWidget                                                    }
{ ============================================================ }

constructor TWLWidget.Create(const ARect: TRectI);
begin
  inherited Create;
  FRect := ARect;
  FParent := nil;
  SetLength(FChildren, 0);
  FVisible := True;
  FEnabled := True;
  FCanFocus := False;
  FFocused := False;
  FName := '';
  FFont := nil;
  FTag := 0;
end;

destructor TWLWidget.Destroy;
begin
  ClearChildren;
  inherited;
end;

function TWLWidget.GetRoot: TWLWidget;
begin
  Result := Self;
  while Result.FParent <> nil do
    Result := Result.FParent;
end;

procedure TWLWidget.AddChild(AChild: TWLWidget);
var
  N: Integer;
begin
  if AChild = nil then Exit;
  if AChild = Self then Exit;
  if AChild.FParent <> nil then
    AChild.FParent.RemoveChild(AChild);

  N := Length(FChildren);
  SetLength(FChildren, N + 1);
  FChildren[N] := AChild;
  AChild.FParent := Self;
end;

procedure TWLWidget.RemoveChild(AChild: TWLWidget);
var
  I, J: Integer;
begin
  for I := 0 to High(FChildren) do
  begin
    if FChildren[I] = AChild then
    begin
      FChildren[I].FParent := nil;
      for J := I to High(FChildren) - 1 do
        FChildren[J] := FChildren[J + 1];
      SetLength(FChildren, Length(FChildren) - 1);
      Exit;
    end;
  end;
end;

procedure TWLWidget.ClearChildren;
var
  I: Integer;
begin
  for I := 0 to High(FChildren) do
  begin
    FChildren[I].FParent := nil;
    FChildren[I].Free;
  end;
  SetLength(FChildren, 0);
end;

function TWLWidget.ScreenRect: TRectI;
var
  ParentRect: TRectI;
begin
  if FParent = nil then
  begin
    Result := FRect;
    Exit;
  end;
  ParentRect := FParent.ScreenRect;
  Result.X := ParentRect.X + FRect.X;
  Result.Y := ParentRect.Y + FRect.Y;
  Result.W := FRect.W;
  Result.H := FRect.H;
end;

function TWLWidget.ClientRect: TRectI;
begin
  Result := TRectI.New(0, 0, FRect.W, FRect.H);
end;

function TWLWidget.HitTest(X, Y: Integer): TWLWidget;
var
  I: Integer;
  Screen: TRectI;
  Child: TWLWidget;
begin
  Result := nil;
  if not FVisible then Exit;

  Screen := ScreenRect;
  if not Screen.Contains(X, Y) then Exit;

  // С конца — последние добавленные сверху
  for I := High(FChildren) downto 0 do
  begin
    Child := FChildren[I].HitTest(X, Y);
    if Child <> nil then Exit(Child);
  end;

  // Если никто из детей не попал и мы focusable/enabled
  if FEnabled then
    Result := Self;
end;

procedure TWLWidget.Paint(ACanvas: TWLCanvas);
var
  I: Integer;
begin
  if not FVisible then Exit;

  DoPaint(ACanvas);

  for I := 0 to High(FChildren) do
    FChildren[I].Paint(ACanvas);
end;

{ --- Мышь --- }

function TWLWidget.DispatchMouseDown(X, Y: Integer): Boolean;
var
  I: Integer;
  Screen: TRectI;
begin
  Result := False;
  if not FVisible then Exit;
  if not FEnabled then Exit;

  Screen := ScreenRect;
  if not Screen.Contains(X, Y) then Exit;

  // Сначала детям
  for I := High(FChildren) downto 0 do
  begin
    if FChildren[I].DispatchMouseDown(X, Y) then
      Exit(True);
  end;

  // Потом себе
  Result := DoMouseDown(X, Y);
end;

function TWLWidget.DispatchMouseUp(X, Y: Integer): Boolean;
var
  I: Integer;
begin
  Result := False;
  if not FVisible then Exit;

  // Сначала детям (все пробуют, кто «захвачен» — тот и обработает)
  for I := 0 to High(FChildren) do
  begin
    if FChildren[I].DispatchMouseUp(X, Y) then
      Exit(True);
  end;

  Result := DoMouseUp(X, Y);
end;

procedure TWLWidget.DispatchMouseMove(X, Y: Integer);
var
  I: Integer;
  Screen: TRectI;
begin
  if not FVisible then Exit;

  // Детям — всегда (для hover)
  for I := 0 to High(FChildren) do
    FChildren[I].DispatchMouseMove(X, Y);

  Screen := ScreenRect;
  if Screen.Contains(X, Y) then
    DoMouseMove(X, Y);
end;

{ --- Клавиатура --- }

function TWLWidget.DispatchKeyDown(const E: TWLKeyEvent): Boolean;
var
  Focused: TWLWidget;
begin
  Result := False;
  if not FVisible then Exit;

  // Если корень — ищем focused виджет
  if FParent = nil then
  begin
    Focused := GetFocusedWidget;
    if Focused <> nil then
    begin
      Focused.DoKeyDown(E);
      Exit(True);
    end;
    Exit(False);
  end;

  // Иначе — на себе
  if FFocused then
  begin
    DoKeyDown(E);
    Result := True;
  end;
end;

function TWLWidget.DispatchKeyUp(const E: TWLKeyEvent): Boolean;
var
  Focused: TWLWidget;
begin
  Result := False;
  if not FVisible then Exit;

  if FParent = nil then
  begin
    Focused := GetFocusedWidget;
    if Focused <> nil then
    begin
      Focused.DoKeyUp(E);
      Exit(True);
    end;
    Exit(False);
  end;

  if FFocused then
  begin
    DoKeyUp(E);
    Result := True;
  end;
end;

{ --- Фокус --- }

function TWLWidget.CanFocus: Boolean;
begin
  Result := FCanFocus and FEnabled and FVisible;
end;

function TWLWidget.GetFocusedWidget: TWLWidget;
var
  I: Integer;
  W: TWLWidget;
begin
  Result := nil;
  if not FVisible then Exit;

  if FFocused then Exit(Self);

  for I := 0 to High(FChildren) do
  begin
    W := FChildren[I].GetFocusedWidget;
    if W <> nil then Exit(W);
  end;
end;

procedure TWLWidget.SetFocus;
var
  R: TWLWidget;
  Prev: TWLWidget;
begin
  if FFocused then Exit;
  if not CanFocus then Exit;

  R := GetRoot;

  // Снять фокус с предыдущего
  Prev := R.GetFocusedWidget;
  if Prev <> nil then
    Prev.ClearFocus;

  // Установить
  FFocused := True;
  DoGotFocus;
end;

procedure TWLWidget.ClearFocus;
begin
  if not FFocused then Exit;
  FFocused := False;
  DoLostFocus;
end;

{ --- Виртуальные (по умолчанию — ничего) --- }

procedure TWLWidget.DoPaint(ACanvas: TWLCanvas);
begin
end;

function TWLWidget.DoMouseDown(X, Y: Integer): Boolean;
begin
  Result := False;
end;

function TWLWidget.DoMouseUp(X, Y: Integer): Boolean;
begin
  Result := False;
end;

procedure TWLWidget.DoMouseMove(X, Y: Integer);
begin
end;

procedure TWLWidget.DoKeyDown(const E: TWLKeyEvent);
begin
end;

procedure TWLWidget.DoKeyUp(const E: TWLKeyEvent);
begin
end;

procedure TWLWidget.DoGotFocus;
begin
end;

procedure TWLWidget.DoLostFocus;
begin
end;

function TWLWidget.GetChild(Index: Integer): TWLWidget;
begin
  if (Index >= 0) and (Index < Length(FChildren)) then
    Result := FChildren[Index]
  else
    Result := nil;
end;

function TWLWidget.GetChildCount: Integer;
begin
  Result := Length(FChildren);
end;

end.

2. wlgui_label.pas
pascal

unit wlgui_label;
{$mode objfpc}{$H+}

interface

uses
  SysUtils,
  wlgui_types, wlgui_canvas, wlgui_font, wlgui_widget;

type
  TWLTextAlign = (taLeft, taCenter, taRight);

  TWLLabel = class(TWLWidget)
  private
    FCaption: String;
    FAlign: TWLTextAlign;
    FTextColor: TWLColor;
  protected
    procedure DoPaint(ACanvas: TWLCanvas); override;
  public
    constructor Create(const ARect: TRectI; const ACaption: String;
                       AFont: TWLFont);
    property Caption: String read FCaption write FCaption;
    property Align: TWLTextAlign read FAlign write FAlign;
    property TextColor: TWLColor read FTextColor write FTextColor;
  end;

implementation

constructor TWLLabel.Create(const ARect: TRectI; const ACaption: String;
  AFont: TWLFont);
begin
  inherited Create(ARect);
  FFont := AFont;
  FCaption := ACaption;
  FAlign := taLeft;
  FTextColor := TWLColor($00FFFFFF);
  // Label не принимает мышь и не берёт фокус — пропускает всё дальше
  FEnabled := False;
end;

procedure TWLLabel.DoPaint(ACanvas: TWLCanvas);
var
  R: TRectI;
  TextW, TextH, TextX, TextY: Integer;
begin
  if (ACanvas = nil) or (FFont = nil) then Exit;
  if FCaption = '' then Exit;

  R := ScreenRect;
  TextW := ACAnvas.TextWidth(FCaption, FFont);
  TextH := FFont.Height;

  case FAlign of
    taCenter: TextX := R.X + (R.W - TextW) div 2;
    taRight:  TextX := R.X + R.W - TextW;
  else
    TextX := R.X;
  end;

  TextY := R.Y + (R.H - TextH) div 2;

  ACAnvas.TextOut(TextX, TextY, FCaption, FTextColor, FFont);
end;

end.

3. wlgui_checkbox.pas
pascal

unit wlgui_checkbox;
{$mode objfpc}{$H+}

interface

uses
  SysUtils,
  wlgui_types, wlgui_canvas, wlgui_font, wlgui_widget;

type
  TWLCheckBox = class(TWLWidget)
  private
    FCaption: String;
    FChecked: Boolean;
    FTextColor: TWLColor;
    FBorderColor: TWLColor;
    FBoxColor: TWLColor;
    FCheckColor: TWLColor;
    FOnChange: TWLNotifyEvent;
    FBoxSize: Integer;
    FSpaceBetween: Integer;
  protected
    procedure DoPaint(ACanvas: TWLCanvas); override;
    function  DoMouseDown(X, Y: Integer): Boolean; override;
    function  DoMouseUp(X, Y: Integer): Boolean; override;
  public
    constructor Create(const ARect: TRectI; const ACaption: String;
                       AFont: TWLFont; AChecked: Boolean = False);
    property Caption: String read FCaption write FCaption;
    property Checked: Boolean read FChecked write FChecked;
    property TextColor: TWLColor read FTextColor write FTextColor;
    property BorderColor: TWLColor read FBorderColor write FBorderColor;
    property BoxColor: TWLColor read FBoxColor write FBoxColor;
    property CheckColor: TWLColor read FCheckColor write FCheckColor;
    property OnChange: TWLNotifyEvent read FOnChange write FOnChange;
  end;

implementation

constructor TWLCheckBox.Create(const ARect: TRectI; const ACaption: String;
  AFont: TWLFont; AChecked: Boolean);
begin
  inherited Create(ARect);
  FFont := AFont;
  FCaption := ACaption;
  FChecked := AChecked;
  FTextColor := TWLColor($00FFFFFF);
  FBorderColor := TWLColor($00A0A0A0);
  FBoxColor := TWLColor($00202020);
  FCheckColor := TWLColor($0040FF40);
  FBoxSize := 16;
  FSpaceBetween := 6;
  FCanFocus := True;
end;

procedure TWLCheckBox.DoPaint(ACanvas: TWLCanvas);
var
  R, BoxR, InnerR: TRectI;
  TextX, TextY: Integer;
  DrawColor: TWLColor;
  BoxY: Integer;
begin
  if (ACanvas = nil) or (FFont = nil) then Exit;

  R := ScreenRect;
  BoxY := R.Y + (R.H - FBoxSize) div 2;
  BoxR := TRectI.New(R.X, BoxY, FBoxSize, FBoxSize);

  if FEnabled then
    DrawColor := FTextColor
  else
    DrawColor := TWLColor($00808080);

  // Фон чекбокса
  ACAnvas.FillRect(BoxR, FBoxColor);

  // Рамка
  ACAnvas.Rect(BoxR, FBorderColor);

  // Галочка
  if FChecked then
  begin
    InnerR := TRectI.New(BoxR.X + 3, BoxR.Y + 3, FBoxSize - 6, FBoxSize - 6);
    ACAnvas.Line(InnerR.X, InnerR.Y + InnerR.H div 2,
                 InnerR.X + InnerR.W div 3, InnerR.Y + InnerR.H - 1,
                 FCheckColor);
    ACAnvas.Line(InnerR.X + InnerR.W div 3, InnerR.Y + InnerR.H - 1,
                 InnerR.X + InnerR.W - 1, InnerR.Y,
                 FCheckColor);
  end;

  // Текст
  if FCaption <> '' then
  begin
    TextX := R.X + FBoxSize + FSpaceBetween;
    TextY := R.Y + (R.H - FFont.Height) div 2;
    ACAnvas.TextOut(TextX, TextY, FCaption, DrawColor, FFont);
  end;
end;

function TWLCheckBox.DoMouseDown(X, Y: Integer): Boolean;
begin
  Result := FEnabled;
end;

function TWLCheckBox.DoMouseUp(X, Y: Integer): Boolean;
begin
  Result := False;
  if not FEnabled then Exit;
  if not ScreenRect.Contains(X, Y) then Exit;

  FChecked := not FChecked;
  if Assigned(FOnChange) then
    FOnChange(Self);
  Result := True;
end;

end.

4. wlgui_edit.pas
pascal

unit wlgui_edit;
{$mode objfpc}{$H+}

interface

uses
  SysUtils,
  wlgui_types, wlgui_canvas, wlgui_font, wlgui_widget, wlgui_events;

type
  TWLEdit = class(TWLWidget)
  private
    FText: String;
    FPosition: Integer;
    FScrollX: Integer;
    FTextColor: TWLColor;
    FBackColor: TWLColor;
    FBorderColor: TWLColor;
    FFocusedBorderColor: TWLColor;
    FCursorColor: TWLColor;
    FOnChange: TWLNotifyEvent;
    FMaxLength: Integer;
    FPadding: Integer;
    procedure UpdateScrollX;
    function TextWidthUpTo(APos: Integer): Integer;
  protected
    procedure DoPaint(ACanvas: TWLCanvas); override;
    function  DoMouseDown(X, Y: Integer): Boolean; override;
    procedure DoKeyDown(const E: TWLKeyEvent); override;
  public
    constructor Create(const ARect: TRectI; const AText: String;
                       AFont: TWLFont);
    function CanFocus: Boolean; override;

    property Text: String read FText write FText;
    property Position: Integer read FPosition write FPosition;
    property MaxLength: Integer read FMaxLength write FMaxLength;
    property OnChange: TWLNotifyEvent read FOnChange write FOnChange;
    property TextColor: TWLColor read FTextColor write FTextColor;
    property BackColor: TWLColor read FBackColor write FBackColor;
    property BorderColor: TWLColor read FBorderColor write FBorderColor;
    property FocusedBorderColor: TWLColor read FFocusedBorderColor
                                       write FFocusedBorderColor;
  end;

implementation

constructor TWLEdit.Create(const ARect: TRectI; const AText: String;
  AFont: TWLFont);
begin
  inherited Create(ARect);
  FFont := AFont;
  FText := AText;
  FPosition := Length(AText);
  FScrollX := 0;
  FTextColor := TWLColor($00FFFFFF);
  FBackColor := TWLColor($00202020);
  FBorderColor := TWLColor($00606060);
  FFocusedBorderColor := TWLColor($00FFFFFF);
  FCursorColor := TWLColor($00FFFFFF);
  FMaxLength := 255;
  FPadding := 4;
  FCanFocus := True;
end;

function TWLEdit.CanFocus: Boolean;
begin
  Result := FEnabled;
end;

function TWLEdit.TextWidthUpTo(APos: Integer): Integer;
var
  Part: String;
begin
  if APos <= 0 then Exit(0);
  if APos > Length(FText) then APos := Length(FText);
  Part := Copy(FText, 1, APos);
  Result := FFont.TextWidth(Part);
end;

procedure TWLEdit.UpdateScrollX;
var
  CursorPixelX, VisibleWidth: Integer;
begin
  if (FFont = nil) or (FRect.W <= 2 * FPadding) then
  begin
    FScrollX := 0;
    Exit;
  end;

  VisibleWidth := FRect.W - 2 * FPadding;
  CursorPixelX := TextWidthUpTo(FPosition) - FScrollX;

  // Курсор ушёл за правый край — прокрутить вправо
  if CursorPixelX > VisibleWidth then
    FScrollX := TextWidthUpTo(FPosition) - VisibleWidth;

  // Курсор ушёл за левый край — прокрутить влево
  if CursorPixelX < 0 then
    FScrollX := TextWidthUpTo(FPosition);

  if FScrollX < 0 then
    FScrollX := 0;
end;

procedure TWLEdit.DoPaint(ACanvas: TWLCanvas);
var
  R, InnerR: TRectI;
  BorderC, TextC: TWLColor;
  TextX, TextY: Integer;
  CursorX: Integer;
begin
  if (ACanvas = nil) or (FFont = nil) then Exit;

  R := ScreenRect;

  // Фон
  ACAnvas.FillRect(R, FBackColor);

  // Рамка
  if FFocused then BorderC := FFocusedBorderColor
  else BorderC := FBorderColor;
  ACAnvas.Rect(R, BorderC);

  // Клип внутри поля
  InnerR := TRectI.New(R.X + 1, R.Y + 1, R.W - 2, R.H - 2);
  ACAnvas.SetClip(InnerR);

  // Текст
  if FEnabled then
    TextC := FTextColor
  else
    TextC := TWLColor($00808080);

  TextX := R.X + FPadding - FScrollX;
  TextY := R.Y + (R.H - FFont.Height) div 2;

  if FText <> '' then
    ACAnvas.TextOut(TextX, TextY, FText, TextC, FFont);

  // Курсор — только при фокусе, мигает
  if FFocused and FEnabled then
  begin
    if (GetTickCount64 div 500) mod 2 = 0 then
    begin
      CursorX := R.X + FPadding + TextWidthUpTo(FPosition) - FScrollX;
      // Только если курсор в видимой области
      if (CursorX >= InnerR.X) and (CursorX < InnerR.X + InnerR.W) then
      begin
        ACAnvas.FillRect(
          TRectI.New(CursorX, InnerR.Y + 2, 1, InnerR.H - 4),
          FCursorColor);
      end;
    end;
  end;

  ACAnvas.ResetClip;
end;

function TWLEdit.DoMouseDown(X, Y: Integer): Boolean;
begin
  Result := False;
  if not FEnabled then Exit;
  if not ScreenRect.Contains(X, Y) then Exit;

  SetFocus;

  // Позиция курсора: ищем символ по X
  // Пока просто — ставим в конец
  FPosition := Length(FText);
  UpdateScrollX;
  Result := True;
end;

procedure TWLEdit.DoKeyDown(const E: TWLKeyEvent);
var
  U: UTF8String;
begin
  case E.Keysym of
    $FF08:   // BackSpace
    begin
      if FPosition > 0 then
      begin
        Delete(FText, FPosition, 1);
        Dec(FPosition);
        UpdateScrollX;
        if Assigned(FOnChange) then FOnChange(Self);
      end;
      Exit;
    end;
    $FFFF:   // Delete
    begin
      if FPosition < Length(FText) then
      begin
        Delete(FText, FPosition + 1, 1);
        UpdateScrollX;
        if Assigned(FOnChange) then FOnChange(Self);
      end;
      Exit;
    end;
    $FF51:   // Left
    begin
      if FPosition > 0 then
      begin
        Dec(FPosition);
        UpdateScrollX;
      end;
      Exit;
    end;
    $FF53:   // Right
    begin
      if FPosition < Length(FText) then
      begin
        Inc(FPosition);
        UpdateScrollX;
      end;
      Exit;
    end;
    $FF50:   // Home
    begin
      FPosition := 0;
      UpdateScrollX;
      Exit;
    end;
    $FF57:   // End
    begin
      FPosition := Length(FText);
      UpdateScrollX;
      Exit;
    end;
    $FF1B:   // Escape — снять фокус
    begin
      ClearFocus;
      Exit;
    end;
    $FF0D:   // Enter
    begin
      // Пока ничего — позже будет событие OnAccept
      Exit;
    end;
    $FF09:   // Tab
    begin
      // Позже — переход к следующему виджету
      Exit;
    end;
  end;

  // Ввод печатного символа
  if (E.Codepoint >= 32) and (E.Codepoint <= $10FFFF) then
  begin
    if Length(FText) < FMaxLength then
    begin
      // Преобразуем codepoint в UTF-8
      U := UnicodeToUTF8(E.Codepoint);
      Insert(U, FText, FPosition + 1);
      Inc(FPosition, Length(U));
      UpdateScrollX;
      if Assigned(FOnChange) then FOnChange(Self);
    end;
  end;
end;

end.

Замечание про UnicodeToUTF8: это встроенная функция FPC (System.UnicodeToUTF8(Codepoint: Cardinal): UTF8String). Она корректно кодирует любой codepoint в UTF-8. Отлично работает для кириллицы.

Но: FPosition теперь байтовая позиция в UTF-8. Для ASCII — это то же, что символьная. Для кириллицы — 2 байта на символ. Delete(FText, FPosition, 1) удалит 1 байт, что сломает UTF-8.

Для правильной работы нужен по-символьный учёт. Это следующий этап — подключим u4str. Пока — работает только ASCII, при вводе кириллицы будет ломаться.

Проверка: если у тебя demo только с ASCII — всё работает.
5. Обновлённый demo_input.pas — тест всех виджетов
pascal

program demo_input;
{$mode objfpc}{$H+}
{$codepage utf8}

uses
  cthreads, SysUtils, Classes,
  wlgui_types, wlgui_context, wlgui_app, wlgui_window, wlgui_canvas, wlgui_font,
  wlgui_events, wlgui_xkb, wlgui_widget, wlgui_button, wlgui_label,
  wlgui_checkbox, wlgui_edit;

var
  MyApp: TWLApplication;
  Win: TWLWindow;
  TitleFont, MonoFont, ButtonFont: TWLFont;
  Frame: Integer = 0;

  MouseX, MouseY: Integer;
  MouseDown: Boolean;

  LblTitle, LblStatus: TWLLabel;
  ChkOption: TWLCheckBox;
  EdtName: TWLEdit;
  BtnOk, BtnCancel: TWLButton;

procedure LogLine(const S: String);
begin
  WriteLn('[demo] ', S);
end;

procedure OnEditChange(Sender: TObject);
begin
  LblStatus.Caption := 'Edit: "' + EdtName.Text + '"';
  LogLine('edit changed: ' + EdtName.Text);
end;

procedure OnCheckChange(Sender: TObject);
begin
  if ChkOption.Checked then
    LogLine('checkbox: ON')
  else
    LogLine('checkbox: OFF');
end;

procedure OnBtnOk(Sender: TObject);
begin
  LogLine('OK pressed, name="' + EdtName.Text +
          '", opt=' + BoolToStr(ChkOption.Checked, 'on', 'off') + '"');
end;

procedure OnBtnCancel(Sender: TObject);
begin
  LogLine('Cancel pressed');
  EdtName.Text := '';
  ChkOption.Checked := False;
  LblStatus.Caption := 'Edit: ""';
end;

procedure OnPaint(Sender: TObject);
var
  W: TWLWindow;
  C: TWLCanvas;
  FillColor: TWLColor;
begin
  W := TWLWindow(Sender);
  if W.BeginPaint = nil then Exit;
  C := W.Canvas;
  if C = nil then Exit;

  C.FillRect(TRectI.New(0, 0, W.Width, W.Height), TWLColor($00181828));

  // Виджеты
  if W.Root <> nil then
    W.Root.Paint(C);

  // Индикатор мыши
  if MouseDown then FillColor := clRed else FillColor := clBlue;
  C.FillRect(TRectI.New(MouseX - 6, MouseY - 6, 12, 12), FillColor);

  // Счётчик кадров внизу
  C.TextOut(20, W.Height - 20,
            Format('frame=%d  mouse=%d,%d  down=%s',
                   [Frame, MouseX, MouseY,
                    BoolToStr(MouseDown, 'yes', 'no')]),
            clDkGray, MonoFont);

  C.EndPaint;
end;

procedure OnMouseDown(Sender: TObject; const E: TWLMouseEvent);
begin
  MouseDown := True;
  MouseX := E.X;
  MouseY := E.Y;
  if (Win <> nil) and (Win.Root <> nil) then
    if Win.Root.DispatchMouseDown(E.X, E.Y) then
      Exit;
  LogLine(Format('window mouse down: %d,%d btn=%d',
                 [E.X, E.Y, E.Button]));
end;

procedure OnMouseUp(Sender: TObject; const E: TWLMouseEvent);
begin
  MouseDown := False;
  MouseX := E.X;
  MouseY := E.Y;
  if (Win <> nil) and (Win.Root <> nil) then
    if Win.Root.DispatchMouseUp(E.X, E.Y) then
      Exit;
end;

procedure OnMouseMove(Sender: TObject; const E: TWLMouseEvent);
begin
  MouseX := E.X;
  MouseY := E.Y;
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchMouseMove(E.X, E.Y);
end;

procedure OnKeyDown(Sender: TObject; const E: TWLKeyEvent);
begin
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchKeyDown(E);
end;

procedure OnKeyUp(Sender: TObject; const E: TWLKeyEvent);
begin
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchKeyUp(E);
end;

procedure OnClose(Sender: TObject);
begin
  MyApp.Quit;
end;

var
  LastTick: QWord;

begin
  WriteLn('wlgui widgets demo');

  MyApp := TWLApplication.Create;
  try
    if not MyApp.Initialize then
    begin
      WriteLn('init failed');
      Halt(1);
    end;

    TitleFont := FontManager.Load('DejaVuSans', 20);
    MonoFont := FontManager.Load('DejaVuSans', 12);
    ButtonFont := FontManager.Load('DejaVuSans', 16);

    Win := CreateWLWindow(MyApp.Context, 'wlgui widgets demo', 640, 480);

    Win.OnPaint := @OnPaint;
    Win.OnMouseDown := @OnMouseDown;
    Win.OnMouseUp := @OnMouseUp;
    Win.OnMouseMove := @OnMouseMove;
    Win.OnKeyDown := @OnKeyDown;
    Win.OnKeyUp := @OnKeyUp;
    Win.OnClose := @OnClose;

    MyApp.Display.Roundtrip;
    Win.Show;

    // Создаём виджеты
    if Win.Root <> nil then
    begin
      // Заголовок
      LblTitle := TWLLabel.Create(
        TRectI.New(20, 20, 600, 30),
        'wlgui widgets demo', TitleFont);
      Win.Root.AddChild(LblTitle);

      // Edit с меткой
      Win.Root.AddChild(TWLLabel.Create(
        TRectI.New(20, 80, 100, 24), 'Name:', MonoFont));

      EdtName := TWLEdit.Create(
        TRectI.New(120, 80, 300, 24), 'Hello', MonoFont);
      EdtName.OnChange := @OnEditChange;
      Win.Root.AddChild(EdtName);

      // CheckBox
      ChkOption := TWLCheckBox.Create(
        TRectI.New(20, 120, 300, 24), 'Enable option', MonoFont, False);
      ChkOption.OnChange := @OnCheckChange;
      Win.Root.AddChild(ChkOption);

      // Кнопки
      BtnOk := TWLButton.Create(
        TRectI.New(20, 180, 140, 36), 'OK', ButtonFont);
      BtnOk.OnClick := @OnBtnOk;
      Win.Root.AddChild(BtnOk);

      BtnCancel := TWLButton.Create(
        TRectI.New(180, 180, 140, 36), 'Cancel', ButtonFont);
      BtnCancel.OnClick := @OnBtnCancel;
      Win.Root.AddChild(BtnCancel);

      // Статус внизу
      LblStatus := TWLLabel.Create(
        TRectI.New(20, 240, 600, 24), 'Edit: "Hello"', MonoFont);
      LblStatus.TextColor := clYellow;
      Win.Root.AddChild(LblStatus);
    end;

    LastTick := GetTickCount64;
    while MyApp.Running do
    begin
      MyApp.ProcessEvents;
      if GetTickCount64 - LastTick >= 33 then
      begin
        LastTick := GetTickCount64;
        Inc(Frame);
        Win.Repaint;
      end;
    end;
  finally
    MyApp.Free;
  end;
end.

Что проверить

    Label — рисуется в правильном месте.

    Edit:

        Клик по Edit → фокус, мигающий курсор.

        Ввод ASCII-символов → появляются.

        Backspace → удаляет.

        Left/Right/Home/End → двигают курсор.

        Escape → снимает фокус.

    CheckBox — клик переключает галочку, OnChange срабатывает.

    Кнопки — работают как раньше.

    Статус — обновляется при изменении Edit.

    Cancel — очищает Edit и CheckBox.

    OK — пишет в лог.

Известные ограничения

    UTF-8: работает для ASCII. Кириллица сломается (Delete(..., 1) удалит байт, не символ). Починим позже.

    Позиция курсора по клику: всегда в конец. Для правильной — нужно искать по X. Позже.

    Tab между виджетами: не работает. Позже.

    Выделение текста: нет. Позже.

Собираем
bash

fpc -Fu. -Fuwlgui demo_input.pas
./demo_input

Запусти, проверь. Если что-то не так — покажи лог или опиши, что видишь. Дальше — либо исправление UTF-8 в Edit (подключение u4str), либо ListBox, либо Tab-навигация.
wlgui_edit.pas(254,12) Error: Wrong number of parameters specified for call to "UnicodeToUtf8"
ustrings.inc(1682,10) Error: Found declaration: UnicodeToUtf8(PChar;PUnicodeChar;Int64):Int64;
ustrings.inc(1691,10) Error: Found declaration: UnicodeToUtf8(PChar;QWord;PUnicodeChar;QWord):QWord; Закомментировал, тогда demo_input.pas(86,5) Error: identifier idents no member "EndPaint"
Две ошибки — обе простые
1. UnicodeToUTF8 в FPC имеет другую сигнатуру

В FPC SysUtils.UnicodeToUTF8 — это не функция Cardinal → UTF8String, а низкоуровневая с параметрами (Dest, Source: PChar, Len). То, что нам нужно, называется UTF8Encode — она принимает WideString/UnicodeString и возвращает UTF8String.

Для codepoint'а проще всего — вручную закодировать:
pascal

function CodepointToUTF8(Code: LongWord): UTF8String;
begin
  if Code < $80 then
    Result := Chr(Code)
  else if Code < $800 then
    Result := Chr($C0 or (Code shr 6)) +
              Chr($80 or (Code and $3F))
  else if Code < $10000 then
    Result := Chr($E0 or (Code shr 12)) +
              Chr($80 or ((Code shr 6) and $3F)) +
              Chr($80 or (Code and $3F))
  else
    Result := Chr($F0 or (Code shr 18)) +
              Chr($80 or ((Code shr 12) and $3F)) +
              Chr($80 or ((Code shr 6) and $3F)) +
              Chr($80 or (Code and $3F));
end;

Замени в wlgui_edit.pas:

В interface — добавь функцию в implementation (приватная):

В implementation, до DoKeyDown:
pascal

function CodepointToUTF8(Code: LongWord): UTF8String;
begin
  if Code < $80 then
    Result := Chr(Code)
  else if Code < $800 then
    Result := Chr($C0 or (Code shr 6)) +
              Chr($80 or (Code and $3F))
  else if Code < $10000 then
    Result := Chr($E0 or (Code shr 12)) +
              Chr($80 or ((Code shr 6) and $3F)) +
              Chr($80 or (Code and $3F))
  else
    Result := Chr($F0 or (Code shr 18)) +
              Chr($80 or ((Code shr 12) and $3F)) +
              Chr($80 or ((Code shr 6) and $3F)) +
              Chr($80 or (Code and $3F));
end;

И в DoKeyDown:
pascal

      U := CodepointToUTF8(E.Codepoint);

вместо U := UnicodeToUTF8(E.Codepoint);.
2. EndPaint — метод не у окна, а у canvas

В demo_input.pas, в OnPaint:
pascal

  C.EndPaint;   // ← ЭТО НЕПРАВИЛЬНО

C — это TWLCanvas. У canvas нет метода EndPaint. EndPaint — это метод окна, и он вызывается автоматически внутри W.Repaint.

Просто удали строку C.EndPaint; из OnPaint.

W.Repaint делает:

    Вызывает OnPaint(W) — твоя функция рисует.

    Вызывает W.EndPaint — автоматически.

Патч
wlgui_edit.pas

Найди в implementation функцию CodepointToUTF8 (её нет — добавь) и замени в DoKeyDown:
pascal

      U := CodepointToUTF8(E.Codepoint);

demo_input.pas

В OnPaint — удали строку:
pascal

  C.EndPaint;   // ← УДАЛИТЬ

Собираем
bash

fpc -Fu. -Fuwlgui demo_input.pas
./demo_input

Должно собраться. Запусти — увидишь окно с Label, Edit, CheckBox, двумя Button, Status Label и индикатором мыши.

Проверь:

    Клик в Edit → курсор мигает.

    Ввод текста → появляется.

    Backspace → удаляет.

    Стрелки → двигают курсор.

    Клик на CheckBox → галочка переключается.

    Кнопки → работают.

    Cancel → очищает поля.

Пришли результат (скриншот или лог), и пойдём к ListBox или починке UTF-8.