Спасибо! Всё ожидаемо работает. Давай пойдём к ListBox.
Этап 8: ListBox

TWLListBox — список элементов с выделением и скроллом. Ключевой контрол для «редактора списка событий».
Что должен уметь

    Список строк (Items)

    Выделение одного элемента (ItemIndex)

    Клик выбирает элемент

    Стрелки вверх/вниз — переход по элементам (если в фокусе)

    Скролл — вертикальная прокрутка колёсиком мыши

    OnChange — при смене выделения

    OnDblClick — при двойном клике (для открытия редактора)

Что не делаем в этой итерации

    Мультивыделение — позже.

    Горизонтальный скролл — позже.

    Группировка, дерево — позже.

    Перетаскивание элементов — позже.

API
pascal

type
  TWLListBox = class(TWLWidget)
  private
    FItems: TStringList;
    FItemIndex: Integer;
    FScrollOffset: Integer;      // сколько строк пропущено сверху
    FTextColor, FBackColor: TWLColor;
    FBorderColor, FFocusedBorderColor: TWLColor;
    FSelectColor, FSelectTextColor: TWLColor;
    FOnChange, FOnDblClick: TWLNotifyEvent;
    FItemHeight: Integer;
    FPadding: Integer;
    FLastClickTime: QWord;       // для детекта double-click
  ...
  public
    constructor Create(...);
    function  AddItem(const S: String): Integer;
    procedure Clear;
    procedure DeleteItem(Index: Integer);
    function  VisibleItemCount: Integer;   // сколько строк влезает
    property Items: TStringList read FItems;
    property ItemIndex: Integer read FItemIndex write SetItemIndex;
    property OnChange: TWLNotifyEvent read FOnChange write FOnChange;
    property OnDblClick: TWLNotifyEvent read FOnDblClick write FOnDblClick;
    ...
  end;

Реализация — wlgui_listbox.pas
pascal

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

interface

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

type
  TWLListBox = class(TWLWidget)
  private
    FItems: TStringList;
    FItemIndex: Integer;
    FScrollOffset: Integer;
    FTextColor: TWLColor;
    FBackColor: TWLColor;
    FBorderColor: TWLColor;
    FFocusedBorderColor: TWLColor;
    FSelectColor: TWLColor;
    FSelectTextColor: TWLColor;
    FOnChange: TWLNotifyEvent;
    FOnDblClick: TWLNotifyEvent;
    FItemHeight: Integer;
    FPadding: Integer;
    FLastClickTime: QWord;
    FLastClickIndex: Integer;

    procedure SetItemIndex(AValue: Integer);
    procedure EnsureVisible;
    function  ItemAtY(AY: Integer): Integer;   // -1, если не попали
    function  VisibleItemCount: Integer;
  protected
    procedure DoPaint(ACanvas: TWLCanvas); override;
    function  DoMouseDown(X, Y: Integer): Boolean; override;
    function  DoMouseUp(X, Y: Integer): Boolean; override;
    procedure DoKeyDown(const E: TWLKeyEvent); override;
    // Для колёсика — нужен доступ к событию wheel
    procedure DoMouseWheel(const E: TWLWheelEvent); virtual;
  public
    constructor Create(const ARect: TRectI; AFont: TWLFont);
    destructor Destroy; override;
    function CanFocus: Boolean; override;

    function  AddItem(const S: String): Integer;
    procedure Clear;
    procedure DeleteItem(AIndex: Integer);

    property Items: TStringList read FItems;
    property ItemIndex: Integer read FItemIndex write SetItemIndex;
    property OnChange: TWLNotifyEvent read FOnChange write FOnChange;
    property OnDblClick: TWLNotifyEvent read FOnDblClick write FOnDblClick;
    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;
    property SelectColor: TWLColor read FSelectColor write FSelectColor;
    property SelectTextColor: TWLColor read FSelectTextColor
                                     write FSelectTextColor;
    property ItemHeight: Integer read FItemHeight write FItemHeight;
  end;

implementation

const
  DOUBLE_CLICK_MS = 400;

constructor TWLListBox.Create(const ARect: TRectI; AFont: TWLFont);
begin
  inherited Create(ARect);
  FFont := AFont;
  FItems := TStringList.Create;
  FItemIndex := -1;
  FScrollOffset := 0;
  FTextColor := TWLColor($00FFFFFF);
  FBackColor := TWLColor($00202020);
  FBorderColor := TWLColor($00606060);
  FFocusedBorderColor := TWLColor($00FFFFFF);
  FSelectColor := TWLColor($000060A0);
  FSelectTextColor := TWLColor($00FFFFFF);
  FItemHeight := 0;    // вычислим из шрифта
  FPadding := 4;
  FLastClickTime := 0;
  FLastClickIndex := -1;
  FCanFocus := True;
end;

destructor TWLListBox.Destroy;
begin
  FItems.Free;
  inherited;
end;

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

function TWLListBox.VisibleItemCount: Integer;
var
  H: Integer;
begin
  H := FItemHeight;
  if H <= 0 then
  begin
    if FFont <> nil then H := FFont.Height + 2
    else H := 16;
  end;
  Result := (FRect.H - 2) div H;
  if Result < 0 then Result := 0;
end;

function TWLListBox.AddItem(const S: String): Integer;
begin
  Result := FItems.Add(S);
end;

procedure TWLListBox.Clear;
begin
  FItems.Clear;
  FItemIndex := -1;
  FScrollOffset := 0;
end;

procedure TWLListBox.DeleteItem(AIndex: Integer);
begin
  if (AIndex < 0) or (AIndex >= FItems.Count) then Exit;
  FItems.Delete(AIndex);
  if FItemIndex >= FItems.Count then
    FItemIndex := FItems.Count - 1;
  EnsureVisible;
end;

procedure TWLListBox.SetItemIndex(AValue: Integer);
begin
  if AValue < -1 then AValue := -1;
  if AValue >= FItems.Count then AValue := FItems.Count - 1;
  if FItemIndex = AValue then Exit;
  FItemIndex := AValue;
  EnsureVisible;
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

procedure TWLListBox.EnsureVisible;
var
  VisCount: Integer;
begin
  if FItemIndex < 0 then Exit;
  VisCount := VisibleItemCount;
  if VisCount <= 0 then Exit;

  if FItemIndex < FScrollOffset then
    FScrollOffset := FItemIndex
  else if FItemIndex >= FScrollOffset + VisCount then
    FScrollOffset := FItemIndex - VisCount + 1;

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

function TWLListBox.ItemAtY(AY: Integer): Integer;
var
  RelY, H: Integer;
begin
  Result := -1;
  RelY := AY - (ScreenRect.Y + 1);
  if RelY < 0 then Exit;
  H := FItemHeight;
  if H <= 0 then
  begin
    if FFont <> nil then H := FFont.Height + 2 else H := 16;
  end;
  Result := FScrollOffset + RelY div H;
  if (Result < 0) or (Result >= FItems.Count) then
    Result := -1;
end;

procedure TWLListBox.DoPaint(ACanvas: TWLCanvas);
var
  R, InnerR, ItemR: TRectI;
  BorderC, TextC: TWLColor;
  VisCount, I, Idx, Y, H: Integer;
  S: String;
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);

  H := FItemHeight;
  if H <= 0 then H := FFont.Height + 2;
  VisCount := InnerR.H div H;

  for I := 0 to VisCount - 1 do
  begin
    Idx := FScrollOffset + I;
    if Idx >= FItems.Count then Break;

    Y := InnerR.Y + I * H;
    ItemR := TRectI.New(InnerR.X, Y, InnerR.W, H);

    if Idx = FItemIndex then
    begin
      ACAnvas.FillRect(ItemR, FSelectColor);
      TextC := FSelectTextColor;
    end
    else
    begin
      if FEnabled then TextC := FTextColor
      else TextC := TWLColor($00808080);
    end;

    S := FItems[Idx];
    ACAnvas.TextOut(InnerR.X + FPadding,
                    Y + (H - FFont.Height) div 2,
                    S, TextC, FFont);
  end;

  ACAnvas.ResetClip;
end;

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

  SetFocus;
  Result := True;
end;

function TWLListBox.DoMouseUp(X, Y: Integer): Boolean;
var
  Idx: Integer;
  Now_: QWord;
begin
  Result := False;
  if not FEnabled then Exit;
  if not ScreenRect.Contains(X, Y) then Exit;

  Idx := ItemAtY(Y);
  if Idx < 0 then Exit;

  Now_ := GetTickCount64;

  // Детект двойного клика
  if (FLastClickIndex = Idx) and
     (Now_ - FLastClickTime <= DOUBLE_CLICK_MS) then
  begin
    // Двойной клик
    SetItemIndex(Idx);
    if Assigned(FOnDblClick) then
      FOnDblClick(Self);
    FLastClickIndex := -1;
    FLastClickTime := 0;
  end
  else
  begin
    SetItemIndex(Idx);
    FLastClickIndex := Idx;
    FLastClickTime := Now_;
  end;

  Result := True;
end;

procedure TWLListBox.DoKeyDown(const E: TWLKeyEvent);
begin
  case E.Keysym of
    $FF52:   // Up
    begin
      if FItemIndex > 0 then
        SetItemIndex(FItemIndex - 1);
      Exit;
    end;
    $FF54:   // Down
    begin
      if FItemIndex < FItems.Count - 1 then
        SetItemIndex(FItemIndex + 1);
      Exit;
    end;
    $FF55:   // Page Up
    begin
      SetItemIndex(FItemIndex - VisibleItemCount);
      Exit;
    end;
    $FF56:   // Page Down
    begin
      SetItemIndex(FItemIndex + VisibleItemCount);
      Exit;
    end;
    $FF50:   // Home
    begin
      SetItemIndex(0);
      Exit;
    end;
    $FF57:   // End
    begin
      SetItemIndex(FItems.Count - 1);
      Exit;
    end;
    $FF0D:   // Enter
    begin
      if Assigned(FOnDblClick) then
        FOnDblClick(Self);
      Exit;
    end;
  end;
end;

procedure TWLListBox.DoMouseWheel(const E: TWLWheelEvent);
begin
  // DeltaY > 0 — вверх, < 0 — вниз
  if E.DeltaY > 0 then
    Dec(FScrollOffset, 3)
  else if E.DeltaY < 0 then
    Inc(FScrollOffset, 3);

  if FScrollOffset < 0 then FScrollOffset := 0;
  if FScrollOffset > FItems.Count - VisibleItemCount then
    FScrollOffset := FItems.Count - VisibleItemCount;
  if FScrollOffset < 0 then FScrollOffset := 0;
end;

end.

Проблема: DoMouseWheel не вызывается

TWLWidget пока не имеет DispatchMouseWheel. Нужно добавить в базовый класс.
Патч wlgui_widget.pas

В protected добавить:
pascal

    procedure DoMouseWheel(const E: TWLWheelEvent); virtual;

В public добавить:
pascal

    function DispatchMouseWheel(const E: TWLWheelEvent): Boolean;

В implementation:
pascal

function TWLWidget.DispatchMouseWheel(const E: TWLWheelEvent): Boolean;
var
  I: Integer;
  Screen: TRectI;
begin
  Result := False;
  if not FVisible then Exit;

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

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

  // Себе
  DoMouseWheel(E);
  Result := True;
end;

procedure TWLWidget.DoMouseWheel(const E: TWLWheelEvent);
begin
end;

И в wlgui_widget.pas, в uses — добавить wlgui_events (уже есть).
Патч demo_input.pas — OnMouseWheel
pascal

procedure OnMouseWheel(Sender: TObject; const E: TWLWheelEvent);
begin
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchMouseWheel(E);
end;

И назначить в конструкторе окна:
pascal

Win.OnMouseWheel := @OnMouseWheel;

Патч wlgui_listbox.pas

У TWLListBox есть приватный метод DoMouseWheel. Он должен быть protected — потому что TWLWidget.DoMouseWheel virtual. У нас сейчас в protected — правильно. Но нужен модификатор override:
pascal

  protected
    ...
    procedure DoMouseWheel(const E: TWLWheelEvent); override;
  ...

Сделай это — иначе вызовется пустой родительский.
Демо — тест ListBox

Заменим содержимое demo или создадим новый demo_listbox.pas:
pascal

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

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

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

  LblTitle, LblStatus: TWLLabel;
  List: TWLListBox;
  BtnAdd, BtnDel, BtnClear: TWLButton;
  NextItemNum: Integer = 1;

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

procedure UpdateStatus;
begin
  if List.ItemIndex >= 0 then
    LblStatus.Caption := Format('Selected #%d: "%s"',
      [List.ItemIndex, List.Items[List.ItemIndex]])
  else
    LblStatus.Caption := Format('%d items, none selected', [List.Items.Count]);
end;

procedure OnListChange(Sender: TObject);
begin
  UpdateStatus;
  LogLine('list change: index=' + IntToStr(List.ItemIndex));
end;

procedure OnListDblClick(Sender: TObject);
begin
  if List.ItemIndex >= 0 then
    LogLine('DOUBLE CLICK on: ' + List.Items[List.ItemIndex]);
end;

procedure OnAddClick(Sender: TObject);
var
  S: String;
begin
  S := 'Event ' + IntToStr(NextItemNum) + ': ' +
       FormatDateTime('hh:nn:ss', Now);
  List.AddItem(S);
  Inc(NextItemNum);
  List.ItemIndex := List.Items.Count - 1;
  UpdateStatus;
end;

procedure OnDelClick(Sender: TObject);
begin
  if List.ItemIndex >= 0 then
    List.DeleteItem(List.ItemIndex);
  UpdateStatus;
end;

procedure OnClearClick(Sender: TObject);
begin
  List.Clear;
  UpdateStatus;
end;

procedure OnPaint(Sender: TObject);
var
  W: TWLWindow;
  C: TWLCanvas;
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);

  C.TextOut(20, W.Height - 20,
            Format('frame=%d', [Frame]),
            clDkGray, MonoFont);
end;

procedure OnMouseDown(Sender: TObject; const E: TWLMouseEvent);
begin
  if (Win <> nil) and (Win.Root <> nil) then
    if Win.Root.DispatchMouseDown(E.X, E.Y) then
      Exit;
end;

procedure OnMouseUp(Sender: TObject; const E: TWLMouseEvent);
begin
  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
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchMouseMove(E.X, E.Y);
end;

procedure OnMouseWheel(Sender: TObject; const E: TWLWheelEvent);
begin
  if (Win <> nil) and (Win.Root <> nil) then
    Win.Root.DispatchMouseWheel(E);
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 listbox demo');

  MyApp := TWLApplication.Create;
  try
    if not MyApp.Initialize then Halt(1);

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

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

    Win.OnPaint := @OnPaint;
    Win.OnMouseDown := @OnMouseDown;
    Win.OnMouseUp := @OnMouseUp;
    Win.OnMouseMove := @OnMouseMove;
    Win.OnMouseWheel := @OnMouseWheel;
    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, 15, 600, 30), 'ListBox demo', TitleFont);
      Win.Root.AddChild(LblTitle);

      List := TWLListBox.Create(
        TRectI.New(20, 60, 400, 300), MonoFont);
      List.OnChange := @OnListChange;
      List.OnDblClick := @OnListDblClick;
      Win.Root.AddChild(List);

      // Наполним примерами
      List.AddItem('Event 1: 09:00 - Утренний отчёт');
      List.AddItem('Event 2: 10:30 - Совещание');
      List.AddItem('Event 3: 12:00 - Обед');
      List.AddItem('Event 4: 14:00 - Работа над проектом');
      List.AddItem('Event 5: 16:30 - Созвон с командой');
      List.AddItem('Event 6: 18:00 - Итоги дня');
      List.AddItem('Event 7: 20:00 - Отдых');
      List.AddItem('Event 8: 22:00 - Чтение');
      List.AddItem('Event 9: 23:30 - Подготовка ко сну');

      BtnAdd := TWLButton.Create(
        TRectI.New(440, 60, 180, 36), 'Add', ButtonFont);
      BtnAdd.OnClick := @OnAddClick;
      Win.Root.AddChild(BtnAdd);

      BtnDel := TWLButton.Create(
        TRectI.New(440, 110, 180, 36), 'Delete', ButtonFont);
      BtnDel.OnClick := @OnDelClick;
      Win.Root.AddChild(BtnDel);

      BtnClear := TWLButton.Create(
        TRectI.New(440, 160, 180, 36), 'Clear', ButtonFont);
      BtnClear.OnClick := @OnClearClick;
      Win.Root.AddChild(BtnClear);

      LblStatus := TWLLabel.Create(
        TRectI.New(20, 380, 600, 30), '9 items, none selected', 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.

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

    Список рисуется с 9 элементами.

    Клик по элементу — выделяется (синий фон).

    Стрелки вверх/вниз — переход между элементами, если List в фокусе (клик по List сначала).

    Home/End — в начало/конец.

    PageUp/PageDown — на страницу.

    Колесо мыши — прокрутка списка.

    Двойной клик — детектится, пишет в лог.

    Кнопки Add/Delete/Clear — манипулируют списком.

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

    Enter — то же, что двойной клик.

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

    Скролл работает, но индикатора прокрутки нет (позже).

    Keyboard focus — если List не в фокусе, стрелки не работают. Клик по элементу — устанавливает фокус.

    Мультивыделение — нет.

    Пустой список — ничего не рисуется.

Сборка
bash

fpc -Fu. -Fuwlgui demo_listbox.pas
./demo_listbox

Патчи, которые нужно применить

    wlgui_widget.pas — добавить DoMouseWheel + DispatchMouseWheel.

    wlgui_listbox.pas — в protected объявить DoMouseWheel с override.

Проверь, что оба патча применены, иначе wheel не заработает.

Запусти, проверь, пришли скриншот или лог. Дальше — либо починка UTF-8 в Edit (подключение u4str), либо обогащение ListBox (иконки, цвета элементов, скроллбар).