Спасибо! Работает. Может быть на основе u4 создадим что-то, что нам понадобится в wlgui? Вот u4helper уже пригодился. А может нужно ещё что-то? Я смотрю, что в wlgui_listbox применяется TStringList, а у нас в u4 вроде нет подобного? И прочее, что может здесь потребоваться.
Этап 10: Анализ — что нам нужно из u4-мира для wlgui

Отличная идея. Давай посмотрим что у нас сейчас используется и что могло бы пригодиться, чтобы создать u4-обёртки для wlgui.
Что у нас уже есть (и что использует что)
Модуль wlgui	Что хранит/использует	Проблема
wlgui_edit.pas	IU4String	✅ уже на u4
wlgui_listbox.pas	TStringList	❌ TStringList — ANSI, не UTF-8-aware
wlgui_button.pas	String (Caption)	❌ ANSI
wlgui_label.pas	String (Caption)	❌ ANSI
wlgui_checkbox.pas	String (Caption)	❌ ANSI
wlgui_widget.pas	—	не хранит текст
wlgui_window.pas	String (Title)	❌ ANSI
wlgui_font.pas	—	уже работает с UTF-8 через FreeType
wlgui_canvas.pas	—	принимает UTF8String и IU4String

Вывод: почти всё, что хранит текст, использует ANSI String (UTF8String с {$codepage utf8}). Это работает, но непоследовательно.
Что нам нужно из u4-мира
1. TU4List — список строк для ListBox

Замена TStringList, но с IU4String внутри.

API:
pascal

type
  TU4List = class
  private
    FItems: array of IU4String;
    FCount: Integer;
  public
    function Add(const S: IU4String): Integer; overload;
    function Add(const S: UTF8String): Integer; overload;
    function Add(const S: String): Integer; overload;   // синоним для UTF8
    procedure Insert(Index: Integer; const S: IU4String);
    procedure Delete(Index: Integer);
    procedure Clear;
    function IndexOf(const S: IU4String): Integer;
    function GetItem(Index: Integer): IU4String;
    procedure SetItem(Index: Integer; const S: IU4String);

    property Items[Index: Integer]: IU4String read GetItem write SetItem; default;
    property Count: Integer read FCount;

    { Сортировка через u4sort. }
    procedure SortOrdinal;
    procedure SortNatural;
    procedure SortNaturalCI;
  end;

Что даёт:

    Хранит Unicode.

    Сортировка через u4sort.

    Поиск через u4str.

    Без конвертации в UTF-8/ANSI.

2. TU4StringBuilder — эффективная сборка строк

Для накопления текста (лог, многострочный редактор, шаблоны).

Проблема сейчас: IU4String.Concat — копирует всю строку при каждом добавлении. Для сборки из 1000 фрагментов — O(n²).

Решение: буфер с SetLength + индекс.
pascal

type
  TU4StringBuilder = class
  private
    FData: array of u4char;
    FCount: Integer;
    FCapacity: Integer;
  public
    procedure Append(C: u4char); overload;
    procedure Append(const S: IU4String); overload;
    procedure Append(const S: UTF8String); overload;
    procedure AppendLine(const S: IU4String);
    procedure Clear;
    function ToString: IU4String;   // создаёт IU4String
    property Count: Integer read FCount;
  end;

Зачем:

    Многострочный Edit — накапливает текст по строкам.

    Лог — добавляет строки.

    JSON/XML writer (позже) — собирает вывод.

    Шаблоны — подставляет значения.

3. TU4Lines — разбивка на строки

Для многострочных текстов. Разделение по \n, склейка.

API:
pascal

type
  TU4Lines = class
  private
    FLines: TU4List;
  public
    procedure SetText(const S: IU4String);   // разбить
    function GetText: IU4String;             // склеить
    function LineCount: Integer;
    function GetLine(Index: Integer): IU4String;
    procedure SetLine(Index: Integer; const S: IU4String);
    procedure AddLine(const S: IU4String);
    procedure InsertLine(Index: Integer; const S: IU4String);
    procedure DeleteLine(Index: Integer);
    ...
  end;

Зачем:

    Многострочный Edit (будущий редактор событий — описание может быть многострочным).

    INI-файлы (позже, для сохранения событий).

    Разбор текста на строки.

4. TU4Buffer — изменяемый буфер символов

Для эффективных insert/delete в Edit.

Проблема IU4String: иммутабелен, каждая вставка — копирование.

Решение:
pascal

type
  TU4Buffer = class
  private
    FData: array of u4char;
    FCount: Integer;      // фактическое число символов
    FCapacity: Integer;   // размер массива
    function GetChar(Index: Integer): u4char;
    procedure SetChar(Index: Integer; C: u4char);
    procedure EnsureCapacity(N: Integer);
  public
    procedure Clear;
    procedure Insert(Pos: Integer; C: u4char); overload;
    procedure Insert(Pos: Integer; const S: IU4String); overload;
    procedure Delete(Pos: Integer; Count: Integer = 1);
    procedure Append(C: u4char); overload;
    procedure Append(const S: IU4String); overload;
    function SubString(Start, Count: Integer): IU4String;
    function ToString: IU4String;
    procedure FromString(const S: IU4String);

    property Chars[Index: Integer]: u4char read GetChar write SetChar;
    property Count: Integer read FCount;
  end;

Зачем:

    Быстрый insert/delete в Edit (O(1) амортизированно для конца, O(n) для середины — но без копирования всей строки).

    Будущий редактор с большими текстами.

5. TU4CaseInsensitiveCompare — для сортировки/поиска без учёта регистра
pascal

function U4CompareCI(const A, B: IU4String): Integer;

Уже есть в u4sort.pas (U4CompareOrdinalCI). Не нужно новое.
6. u4helper.pas — что ещё добавить

К уже имеющемуся:
pascal

{ Разбиение/склейка — уже есть в u4str, но удобные обёртки: }
function U4ReplaceAll(const S, Find, Replace: IU4String): IU4String;
function U4TrimLeft(const S: IU4String): IU4String;
function U4TrimRight(const S: IU4String): IU4String;
function U4StartsWithCI(const S, Prefix: IU4String): Boolean;
function U4ContainsCI(const S, Sub: IU4String): Boolean;

{ Байтовые операции с позицией: }
function U4BytePosOfChar(const S: IU4String; CharPos: Integer): Integer;
function U4CharPosOfByte(const S: IU4String; BytePos: Integer): Integer;

Что действительно нужно сейчас

Расставим приоритеты:
Модуль	Сейчас	Скоро	Позже
TU4List	✅ для ListBox	✅ для меню, комбо	✅ везде
TU4StringBuilder	⚠ для будущего Edit	✅ для логов	✅
TU4Lines	⚠	✅ для многострочного Edit	✅
TU4Buffer	⚠ оптимизация	✅ для больших текстов	✅
Дополнительные функции	✅	✅	✅

Мой план:

    u4list.pas — TU4List (сразу заменим в ListBox).

    u4builder.pas — TU4StringBuilder (пригодится в будущем).

    u4lines.pas — TU4Lines (для многострочного).

    Расширить u4helper.pas — удобные функции.

    u4buffer.pas — позже, когда реально понадобится.

Начнём с TU4List — он немедленно заменит TStringList в ListBox и даст правильный Unicode.
u4list.pas — полная реализация
pascal

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

interface

uses
  Classes, SysUtils,
  u4intf, u4str, u4utf8, u4sort;

type
  { Простой список IU4String — аналог TStringList, но Unicode-aware. }
  TU4List = class
  private
    FItems: array of IU4String;
    FCount: Integer;
    FOwnsObjects: Boolean;

    function  GetItem(Index: Integer): IU4String;
    procedure SetItem(Index: Integer; const AValue: IU4String);
    procedure EnsureCapacity(N: Integer);
  public
    constructor Create;
    destructor Destroy; override;

    { Основные операции. }
    function  Add(const S: IU4String): Integer; overload;
    function  Add(const S: UTF8String): Integer; overload;
    function  Add(const S: String): Integer; overload;   // равно UTF8
    procedure Insert(Index: Integer; const S: IU4String);
    procedure Delete(Index: Integer);
    procedure Clear;
    procedure Assign(ASource: TU4List);

    { Поиск. }
    function  IndexOf(const S: IU4String): Integer;
    function  IndexOfCI(const S: IU4String): Integer;  // без учёта регистра
    function  IndexOfPrefix(const Prefix: IU4String): Integer;

    { Импорт/экспорт через UTF-8. }
    procedure LoadFromUTF8(const S: UTF8String);   // разбивает по \n
    function  ToUTF8: UTF8String;                  // склеивает через \n
    procedure AddFromUTF8Lines(const S: UTF8String);

    { Сортировка. }
    procedure SortOrdinal;
    procedure SortOrdinalCI;
    procedure SortNatural;
    procedure SortNaturalCI;

    { Свойства. }
    property Items[Index: Integer]: IU4String read GetItem write SetItem; default;
    property Count: Integer read FCount;
  end;

implementation

constructor TU4List.Create;
begin
  inherited Create;
  FCount := 0;
  SetLength(FItems, 0);
end;

destructor TU4List.Destroy;
begin
  Clear;
  inherited;
end;

procedure TU4List.EnsureCapacity(N: Integer);
var
  NewCap: Integer;
begin
  if N <= Length(FItems) then Exit;
  NewCap := Length(FItems);
  if NewCap < 16 then NewCap := 16;
  while NewCap < N do
    NewCap := NewCap * 2;
  SetLength(FItems, NewCap);
end;

function TU4List.GetItem(Index: Integer): IU4String;
begin
  if (Index < 0) or (Index >= FCount) then
    Result := nil
  else
    Result := FItems[Index];
end;

procedure TU4List.SetItem(Index: Integer; const AValue: IU4String);
begin
  if (Index < 0) or (Index >= FCount) then Exit;
  FItems[Index] := AValue;
end;

function TU4List.Add(const S: IU4String): Integer;
begin
  EnsureCapacity(FCount + 1);
  FItems[FCount] := S;
  Result := FCount;
  Inc(FCount);
end;

function TU4List.Add(const S: UTF8String): Integer;
begin
  Result := Add(UTF8ToU4(S));
end;

function TU4List.Add(const S: String): Integer;
begin
  Result := Add(UTF8ToU4(S));
end;

procedure TU4List.Insert(Index: Integer; const S: IU4String);
var
  I: Integer;
begin
  if Index < 0 then Index := 0;
  if Index > FCount then Index := FCount;

  EnsureCapacity(FCount + 1);
  for I := FCount downto Index + 1 do
    FItems[I] := FItems[I - 1];
  FItems[Index] := S;
  Inc(FCount);
end;

procedure TU4List.Delete(Index: Integer);
var
  I: Integer;
begin
  if (Index < 0) or (Index >= FCount) then Exit;
  for I := Index to FCount - 2 do
    FItems[I] := FItems[I + 1];
  FItems[FCount - 1] := nil;
  Dec(FCount);
end;

procedure TU4List.Clear;
var
  I: Integer;
begin
  for I := 0 to FCount - 1 do
    FItems[I] := nil;
  FCount := 0;
end;

procedure TU4List.Assign(ASource: TU4List);
var
  I: Integer;
begin
  Clear;
  if ASource = nil then Exit;
  EnsureCapacity(ASource.Count);
  for I := 0 to ASource.Count - 1 do
    Add(ASource[I]);
end;

function TU4List.IndexOf(const S: IU4String): Integer;
var
  I: Integer;
begin
  for I := 0 to FCount - 1 do
    if U4CompareOrdinal(FItems[I], S) = 0 then
      Exit(I);
  Result := -1;
end;

function TU4List.IndexOfCI(const S: IU4String): Integer;
var
  I: Integer;
begin
  for I := 0 to FCount - 1 do
    if U4CompareOrdinalCI(FItems[I], S) = 0 then
      Exit(I);
  Result := -1;
end;

function TU4List.IndexOfPrefix(const Prefix: IU4String): Integer;
var
  I: Integer;
begin
  for I := 0 to FCount - 1 do
    if U4StartsWith(FItems[I], Prefix) then
      Exit(I);
  Result := -1;
end;

procedure TU4List.LoadFromUTF8(const S: UTF8String);
var
  Lines: TStringArray;
  I: Integer;
begin
  Clear;
  if S = '' then Exit;
  Lines := S.Split([#10]);
  for I := 0 to High(Lines) do
  begin
    // Убираем \r
    if (Length(Lines[I]) > 0) and (Lines[I][Length(Lines[I])] = #13) then
      Delete(Lines[I], Length(Lines[I]), 1);
    Add(Lines[I]);
  end;
end;

function TU4List.ToUTF8: UTF8String;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to FCount - 1 do
  begin
    if I > 0 then
      Result := Result + #10;
    Result := Result + U4ToUTF8(FItems[I]);
  end;
end;

procedure TU4List.AddFromUTF8Lines(const S: UTF8String);
var
  Lines: TStringArray;
  I: Integer;
begin
  if S = '' then Exit;
  Lines := S.Split([#10]);
  for I := 0 to High(Lines) do
  begin
    if (Length(Lines[I]) > 0) and (Lines[I][Length(Lines[I])] = #13) then
      Delete(Lines[I], Length(Lines[I]), 1);
    Add(Lines[I]);
  end;
end;

{ --- Сортировка --- }

procedure TU4List.SortOrdinal;
var
  I, J: Integer;
  Tmp: IU4String;
begin
  // Простой bubble sort — заменим на что-то быстрое позже
  for I := 0 to FCount - 2 do
    for J := I + 1 to FCount - 1 do
      if U4CompareOrdinal(FItems[I], FItems[J]) > 0 then
      begin
        Tmp := FItems[I];
        FItems[I] := FItems[J];
        FItems[J] := Tmp;
      end;
end;

procedure TU4List.SortOrdinalCI;
var
  I, J: Integer;
  Tmp: IU4String;
begin
  for I := 0 to FCount - 2 do
    for J := I + 1 to FCount - 1 do
      if U4CompareOrdinalCI(FItems[I], FItems[J]) > 0 then
      begin
        Tmp := FItems[I];
        FItems[I] := FItems[J];
        FItems[J] := Tmp;
      end;
end;

procedure TU4List.SortNatural;
var
  I, J: Integer;
  Tmp: IU4String;
begin
  for I := 0 to FCount - 2 do
    for J := I + 1 to FCount - 1 do
      if U4CompareNatural(FItems[I], FItems[J]) > 0 then
      begin
        Tmp := FItems[I];
        FItems[I] := FItems[J];
        FItems[J] := Tmp;
      end;
end;

procedure TU4List.SortNaturalCI;
var
  I, J: Integer;
  Tmp: IU4String;
begin
  for I := 0 to FCount - 2 do
    for J := I + 1 to FCount - 1 do
      if U4CompareNaturalCI(FItems[I], FItems[J]) > 0 then
      begin
        Tmp := FItems[I];
        FItems[I] := FItems[J];
        FItems[J] := Tmp;
      end;
end;

end.

Замечание: S.Split([#10]) — использует TStringArray из FPC RTL. Если не нравится — заменим на ручное разбиение через Pos.
Патч wlgui_listbox.pas — используем TU4List

Замени:

    FItems: TStringList → FItems: TU4List

    FItems.Add(S) → FItems.Add(S) (работает — есть Add(UTF8String))

    FItems[Index] → FItems[Index] (работает — есть default property)

    FItems.Count → FItems.Count (то же)

    FItems.Delete(I) → FItems.Delete(I) (то же)

    FItems.Clear → FItems.Clear (то же)

Изменится только uses — Classes не нужен, вместо него u4list.

Проверь: строки в ListBox рисуются через TextOut, который принимает UTF8String. Но у нас теперь IU4String. Проверь, что ACanvas.TextOut(..., IU4String, ...) есть (у нас есть overload).

В DoPaint:
pascal

// Сейчас:
S := FItems[Idx];
ACAnvas.TextOut(..., S, TextC, FFont);   // S: String (UTF8)

// Стало:
ACanvas.TextOut(..., FItems[Idx], TextC, FFont);   // FItems[Idx]: IU4String

Что это даёт

    ListBox теперь правильно работает с любым Unicode.

    Сортировка — через u4sort.

    Поиск — через u4str.

    Без конвертаций.

Следующие шаги

После u4list:

    u4builder.pas — TU4StringBuilder для логов и многострочного текста.

    u4lines.pas — TU4Lines для многострочного Edit.

    u4buffer.pas — TU4Buffer для быстрого Edit (позже).

    Расширить u4helper.pas — удобные обёртки.

Что не нужно делать

    Свой TStringList — уже есть TU4List.

    Свой TStringBuilder — есть в u4? Нет. Нужно создать.

    Свои regexp — есть u4regex.pas.

    Свой Trim — есть U4Trim (метод IU4String.Trim).

    Свой Split — есть U4Split, U4SplitStr.

Возможно, что-то из перечисленного уже есть в подгруженных u4-модулях. Проверь — и я не буду дублировать.
Вопросы к тебе

    Начинаем с u4list.pas? Это 5 минут работы + патч ListBox.

    Или сразу все четыре модуля (list, builder, lines, buffer)?

    Что ещё из u4 у нас есть, что может пригодиться в wlgui?

        u4regex — поиск/замена в Edit позже.

        u4json/u4xml/u4ini — сохранение событий.

        u4template — шаблоны уведомлений будильника.

        u4date — форматирование времени событий.

        u4color — цвета в UI.

        u4num — форматирование чисел.

Мой план: u4list сейчас, потом builder, потом lines, потом остальное — по мере необходимости в органайзере.

Скажи, что берём сразу, что отложить. Или, если хочешь, давай сначала посмотрим, что уже есть в подгруженных модулях — чтобы не дублировать.
u4list.pas(196,14) Error: Illegal qualifier
u4list.pas(201,44) Error: Wrong number of parameters specified for call to "Delete"
u4list.pas(128,19) Error: Found declaration: Delete(LongInt);
u4list.pas(225,14) Error: Illegal qualifier
u4list.pas(229,44) Error: Wrong number of parameters specified for call to "Delete"
u4list.pas(128,19) Error: Found declaration: Delete(LongInt);
Две проблемы в u4list.pas — обе косметические
Проблема 1: Lines[I][Length(Lines[I])] — Illegal qualifier

Lines — это TStringArray (массив String). Lines[I] — это String. Индексирование строки как массива Lines[I][N] в FPC не работает (только через [] непосредственно на переменной типа String).

Правильно — использовать Copy или Delete напрямую:
pascal

// Было:
if (Length(Lines[I]) > 0) and (Lines[I][Length(Lines[I])] = #13) then
  Delete(Lines[I], Length(Lines[I]), 1);

// Стало:
var
  L: String;
begin
  L := Lines[I];
  if (Length(L) > 0) and (L[Length(L)] = #13) then
    Delete(L, Length(L), 1);
  Add(L);
end;

Проблема 2: Delete — конфликт имён

Delete — есть метод класса TU4List.Delete(Index: Integer). Внутри класса Delete(Lines[I], ...) — компилятор видит свой метод, а не встроенный System.Delete.

Решение: вызывать System.Delete явно.
Патч u4list.pas — обе процедуры LoadFromUTF8 и AddFromUTF8Lines

Заменяем обе на безопасную версию:
pascal

procedure TU4List.LoadFromUTF8(const S: UTF8String);
var
  I, Start: Integer;
  L: String;
begin
  Clear;
  if S = '' then Exit;

  Start := 1;
  for I := 1 to Length(S) do
  begin
    if S[I] = #10 then
    begin
      L := Copy(S, Start, I - Start);
      if (Length(L) > 0) and (L[Length(L)] = #13) then
        System.Delete(L, Length(L), 1);
      Add(L);
      Start := I + 1;
    end;
  end;
  // Последняя строка
  if Start <= Length(S) then
  begin
    L := Copy(S, Start, Length(S) - Start + 1);
    if (Length(L) > 0) and (L[Length(L)] = #13) then
      System.Delete(L, Length(L), 1);
    Add(L);
  end;
end;

function TU4List.ToUTF8: UTF8String;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to FCount - 1 do
  begin
    if I > 0 then
      Result := Result + #10;
    Result := Result + U4ToUTF8(FItems[I]);
  end;
end;

procedure TU4List.AddFromUTF8Lines(const S: UTF8String);
var
  I, Start: Integer;
  L: String;
begin
  if S = '' then Exit;

  Start := 1;
  for I := 1 to Length(S) do
  begin
    if S[I] = #10 then
    begin
      L := Copy(S, Start, I - Start);
      if (Length(L) > 0) and (L[Length(L)] = #13) then
        System.Delete(L, Length(L), 1);
      Add(L);
      Start := I + 1;
    end;
  end;
  if Start <= Length(S) then
  begin
    L := Copy(S, Start, Length(S) - Start + 1);
    if (Length(L) > 0) and (L[Length(L)] = #13) then
      System.Delete(L, Length(L), 1);
    Add(L);
  end;
end;

Ключевые изменения:

    Ручное разбиение по #10 через Copy.

    System.Delete — явное указание пространства имён.

    Убрано Split (не нужен).

Собери снова
bash

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

Должно собраться. Если ещё что-то — присылай.
Замечание про Default property и синтаксис

Если снова возникнет проблема с FItems[I] — проверь, что у нас есть:
pascal

property Items[Index: Integer]: IU4String read GetItem write SetItem; default;

Слово default делает это свойство основным, и FItems[I] = FItems.Items[I]. Должно работать.

Если не работает — используй явно FItems.Items[I].

Присылай результат — если всё ок, перейдём к u4builder (или сразу к тесту кириллицы в ListBox).
Когда щёлкаю по итему: Starting program: /tmp/demo_listbox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
wlgui listbox demo
[wlgui] EventManager created
[xkb] libxkbcommon loaded
[xkb] context created
[wlgui] Wayland initialized
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=20 valid=TRUE builtin=FALSE ascent=19 descent=5 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=14 valid=TRUE builtin=FALSE ascent=13 descent=3 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=16 valid=TRUE builtin=FALSE ascent=15 descent=4 units_per_EM=2048
[wlgui] window created: "wlgui listbox demo" 640x480
[events] seat name: default
[events] seat capabilities=3
[cursor] libwayland-cursor loaded
[cursor] theme loaded: "" size=24
[cursor] cursor surface created
[events] pointer attached
[events] keyboard attached
[wlgui] 2 buffers created: 640x480 stride=2560
[events] keyboard keymap format=1 size=38030
[xkb] keymap loaded

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
(gdb) bt
#0  0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
#1  0x00007ffff7f55a6e in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
#2  0x00007ffff7f5628e in wl_proxy_destroy () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
#3  0x000055555564ec35 in ?? ()
#4  0x0000000000000000 in ?? ()
Сегфолт в wl_proxy_destroy — двойное освобождение

Смотрим стек:
text

#2  wl_proxy_destroy
#3  0x000055555564ec35  ← наш код

wl_proxy_destroy вызывается дважды для одного объекта, или — что вероятнее — освобождается объект, который нам не принадлежит.
Что мы делаем не так

Смотрим TWLProxyObject.Destroy:
pascal

destructor TWLProxyObject.Destroy;
begin
  inherited Destroy;
  if Assigned(FProxy) and FOwnsProxy then
    wl_proxy_destroy(FProxy);
end;

FOwnsProxy должен быть False для объектов, которые мы не создавали — например, для wl_cursor_image_get_buffer. Но — сегфолт не в курсоре. Он в demo_listbox, при клике по элементу.

Что происходит при клике:

    MouseDown → DispatchMouseDown → ListBox.DoMouseDown → SetFocus.

    MouseUp → DispatchMouseUp → ListBox.DoMouseUp → SetItemIndex → OnChange.

OnChange у нас — UpdateStatus + LogLine. Ничего страшного.

Но — SetFocus! Смотрим:
pascal

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;

GetFocusedWidget — рекурсивно ищет focused. У нас несколько виджетов (Label, ListBox, кнопки). Первый focused — ListBox (если до этого кликали по нему). Prev := ListBox.

Prev.ClearFocus → ListBox.FFocused := False.

Всё чисто.
Где настоящая проблема

Смотрим внимательнее на стек:
text

#0  libwayland-client (внутренняя функция)
#1  libwayland-client (внутренняя функция)
#2  wl_proxy_destroy
#3  0x000055555564ec35   ← адрес в нашем коде

Адрес 0x55555564ec35 — наш исполняемый код. Мы вызываем wl_proxy_destroy. Из какого метода?

Гипотеза: TWLProxyObject.Destroy вызывается для объекта, у которого FOwnsProxy = True, но FProxy уже был уничтожен (либо композитором, либо нами же ранее).

Что могло произойти:

TWLCursorTheme и TWLCursor — держат wl_cursor_theme и wl_surface. При закрытии окна или при определённых действиях — что-то освобождается дважды.

Но при клике? Клик не разрушает окно. Странно.

Смотрим ещё раз: возможно, проблема в OnChange — я вижу, что у нас в wlgui_listbox.pas есть:
pascal

procedure TWLListBox.SetItemIndex(AValue: Integer);
begin
  ...
  FItemIndex := AValue;
  EnsureVisible;
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

FOnChange → OnListChange → UpdateStatus → LblStatus.Caption := ....

LblStatus — может быть nil, если не создан! Проверим demo:
pascal

LblStatus := TWLLabel.Create(
  TRectI.New(20, 380, 600, 30), '9 items, none selected', MonoFont);

Создан. Ок.
Что реально происходит

Смотрим внимательно: сегфолт в wl_proxy_destroy. То есть наш код вызывает wl_proxy_destroy, и он падает.

Причина падения в wl_proxy_destroy: повреждена память. Указатель FProxy уже не валиден.

Как он стал невалидным? Возможные сценарии:

    Двойное освобождение (мы освободили — потом кто-то ещё).

    Перезапись памяти — где-то пишем вне границ массива.

    Использование после освобождения — объект удалён, но мы с ним работаем.

Проверим гипотезу № 2 (перезапись памяти)

Смотрим wlgui_listbox.pas:
pascal

private
  FItems: TStringList;   ← или TU4List

FItems освобождается в destructor — только при уничтожении TWLListBox. Не при клике.

Смотрим EnsureCapacity:
pascal

procedure TU4List.EnsureCapacity(N: Integer);
var
  NewCap: Integer;
begin
  if N <= Length(FItems) then Exit;
  NewCap := Length(FItems);
  if NewCap < 16 then NewCap := 16;
  while NewCap < N do
    NewCap := NewCap * 2;
  SetLength(FItems, NewCap);
end;

FItems — массив IU4String. SetLength — правильно увеличивает.

Add:
pascal

function TU4List.Add(const S: IU4String): Integer;
begin
  EnsureCapacity(FCount + 1);
  FItems[FCount] := S;
  Result := FCount;
  Inc(FCount);
end;

Ок.

GetItem:
pascal

function TU4List.GetItem(Index: Integer): IU4String;
begin
  if (Index < 0) or (Index >= FCount) then
    Result := nil
  else
    Result := FItems[Index];
end;

Ок.
Ищем точное место

Собери demo с отладочной информацией:
bash

fpc -g -gl -Fu. -Fuwlgui demo_listbox.pas

Запусти под gdb:
bash

gdb ./demo_listbox

В gdb:
text

(gdb) run
...
(повтори клик, чтобы упало)
(gdb) bt full

bt full покажет все локальные переменные каждого фрейма — увидим, какой объект освобождается.
Альтернативная проверка: проверим, что клик — источник

Закомментируй OnListChange:
pascal

List.OnChange := @OnListChange;   // ← закомментировать

Запусти. Если падение исчезло — значит проблема в OnChange (или в том, что он вызывает).
Мои две главные гипотезы
Гипотеза A: OnChange вызывается при SetItemIndex(-1) в конструкторе

Смотрим TWLListBox.Create:
pascal

constructor TWLListBox.Create(const ARect: TRectI; AFont: TWLFont);
begin
  ...
  FItemIndex := -1;
  ...
end;

Ок, FItemIndex := -1 напрямую, не через SetItemIndex. OnChange не вызывается.

Но Clear:
pascal

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

Тоже напрямую.
Гипотеза B: U4ToUTF8 при Format в UpdateStatus

UpdateStatus:
pascal

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;

List.Items[List.ItemIndex] — возвращает IU4String. Format('%s', [IU4String]) — проблема! Format не знает про IU4String. Он попытается интерпретировать указатель на интерфейс как PChar — мусор → падение.

Стоп — это наш String-based Format. Наш List.Items возвращает IU4String. Это может быть источник!

Проверка: замени UpdateStatus:
pascal

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

И в OnListDblClick:
pascal

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

И в OnAddClick — создаёт S: String, добавляет через List.AddItem(S) → AddItem принимает String. Ок.

И в OnDelClick — List.DeleteItem(List.ItemIndex) — работает.
Проблема — Format с IU4String

Это классическая ошибка. Format не знает про интерфейсы. Нужно явно преобразовать в UTF8String через U4ToUTF8.
Полный патч demo_listbox.pas

В uses добавь:
pascal

  u4utf8,

Замени UpdateStatus:
pascal

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

Замени OnListDblClick:
pascal

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

Замени OnAddClick:
pascal

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;

AddItem — у нас метод ListBox, принимает String (UTF-8). Работает.
Проверь TWLListBox.AddItem

В wlgui_listbox.pas у нас:
pascal

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

FItems: TU4List — есть Add(String), который вызывает UTF8ToU4. Работает.
Что делать

    Патчи demo_listbox.pas — везде, где Items[...] используется как String, оборачивай в U4ToUTF8.

    Собери, запусти, проверь клик.

    Если всё ещё падает — пришли bt full из gdb.

Общее правило для будущего

IU4String — это интерфейс, не строка.

    Никогда не передавай в Format, WriteLn, +, Concat напрямую.

    Всегда через U4ToUTF8 → UTF8String → дальше.

    Или через u4str функции (U4Join, U4ToLower, и т.д.).

Для String-совместимости: IU4StringHelper из u4wrap.pas имеет .ToUTF8:
pascal

uses u4wrap;
WriteLn(List.Items[List.ItemIndex].ToUTF8);

Красивее. Используй .ToUTF8 вместо U4ToUTF8(...).
Итог

Основная проблема — Format('%s', [IU4String]) → падение не в Format, а внутри libwayland (потому что повреждена память от неправильной интерпретации интерфейса).

Патч: обернуть все IU4String в .ToUTF8 при выводе.

Сделай патч, пришли результат. Если снова падение — пришли bt full, посмотрим точнее.
Starting program: /tmp/demo_listbox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
wlgui listbox demo
[wlgui] EventManager created
[xkb] libxkbcommon loaded
[xkb] context created
[wlgui] Wayland initialized
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=20 valid=TRUE builtin=FALSE ascent=19 descent=5 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=14 valid=TRUE builtin=FALSE ascent=13 descent=3 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=16 valid=TRUE builtin=FALSE ascent=15 descent=4 units_per_EM=2048
[wlgui] window created: "wlgui listbox demo" 640x480
[events] seat name: default
[events] seat capabilities=3
[cursor] libwayland-cursor loaded
[cursor] theme loaded: "" size=24
[cursor] cursor surface created
[events] pointer attached
[events] keyboard attached
[wlgui] 2 buffers created: 640x480 stride=2560
[events] keyboard keymap format=1 size=38030
[xkb] keymap loaded

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
(gdb) bt full
#0  0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#1  0x00007ffff7f55a6e in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#2  0x00007ffff7f5628e in wl_proxy_destroy () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#3  0x0000555555652ba5 in DESTROY (this=0x7ffff7f9c880, vmt=0x0) at wayland_client_core.pas:476
No locals.
#4  0x0000555555654b58 in DESTROY (this=0x7ffff7f9c880, vmt=0x1) at wayland_protocol.pas:1216
No locals.
#5  0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#6  0x0000555555600e17 in SYSUTILS_$$_FREEANDNIL$formal ()
No symbol table info available.
#7  0x000055555565b678 in DESTROY (this=0x7ffff7fac3a0, vmt=0x1) at wlgui_cursor.pas:300
No locals.
#8  0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#9  0x0000555555600e17 in SYSUTILS_$$_FREEANDNIL$formal ()
No symbol table info available.
#10 0x0000555555636898 in DESTROY (this=0x7ffff7f75040, vmt=0x1) at wlgui_events.pas:628
No locals.
#11 0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#12 0x000055555562dfa8 in DESTROY (this=0x7ffff7f8d1c0, vmt=0x1) at wlgui_context.pas:57
No locals.
#13 0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#14 0x0000555555600e17 in SYSUTILS_$$_FREEANDNIL$formal ()
No symbol table info available.
#15 0x000055555562e91a in DESTROY (this=0x7ffff7f9c280, vmt=0x1) at wlgui_app.pas:139
        I = 0
#16 0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#17 0x00005555555b039a in main () at demo_listbox.pas:220
No locals.
🎯 Нашли! Двойное освобождение wl_cursor — уничтожение wl_surface курсора

Смотрим стек:
text

#15 TWLApplication.Destroy (wlgui_app.pas:139)
#14 FreeAndNil
#12 TWLContext.Destroy (wlgui_context.pas:57)   ← Events.Free
#10 TWLEventManager.Destroy (wlgui_events.pas:628)  ← освобождает FCursor
#7  TWLCursor.Destroy (wlgui_cursor.pas:300)        ← FreeAndNil(FSurface)
#4  TWlSurface.Destroy (wayland_protocol.pas:1216)  ← wl_proxy_destroy
#2  wl_proxy_destroy
#0  SIGSEGV

wl_surface для курсора освобождается, и wl_proxy_destroy падает.
Почему это падает

Смотрим TWLCursor.Destroy (строка 300):
pascal

destructor TWLCursor.Destroy;
begin
  if FSurface <> nil then FreeAndNil(FSurface);
  inherited;
end;

FSurface — это TWlSurface — обёртка. FreeAndNil вызывает TWlSurface.Destroy, который:
pascal

destructor TWlSurface.Destroy;
begin
  wl_proxy_marshal(FProxy, _DESTROY);
  inherited Destroy;   ← вызывает TWLProxyObject.Destroy → wl_proxy_destroy
end;

wl_proxy_destroy падает. Почему?

Гипотеза: wl_surface для курсора был создан с OwnsProxy = True (по умолчанию в TWLProxyObject.Create). Значит, при FreeAndNil(FSurface) мы уничтожаем его.

Но: этот wl_surface был привязан к wl_pointer.set_cursor. Композитор его использует. Когда мы вызываем wl_proxy_destroy, libwayland пытается удалить объект из своей таблицы, но что-то не так.

Точнее: при выходе из программы мы уничтожаем всё, включая wl_cursor. wl_cursor_theme_destroy в TWLCursorTheme уже освобождает свои ресурсы. И wl_cursor_theme_destroy может освободить wl_surface курсора (внутри себя). А мы потом пытаемся её освободить — double free.
Смотрим TWLCursorTheme.Destroy
pascal

destructor TWLCursorTheme.Destroy;
begin
  if (FTheme <> nil) and (wl_cursor_theme_destroy <> nil) then
    wl_cursor_theme_destroy(FTheme);
  FTheme := nil;
  inherited;
end;

Порядок в TWLEventManager.Destroy (строка 628, где падает):
pascal

destructor TWLEventManager.Destroy;
begin
  if FCursor <> nil then FreeAndNil(FCursor);       ← Первый
  if FCursorTheme <> nil then FreeAndNil(FCursorTheme);   ← Второй
  ...
end;

Мы освобождаем FCursor ПЕРЕД FCursorTheme. Но wl_cursor (полученный из темы) принадлежит теме. wl_cursor_theme_destroy освободит все свои курсоры, включая тот, что мы используем.

Значит: wl_surface, которую мы используем для курсора, не освобождается темой. Тема освобождает wl_cursor (метаданные), но не буферы и не surface. Surface наша, мы её создали через FCompositor.CreateSurface.

Тогда почему падает?
Более вероятная причина: wl_buffer уже освобождён темой

wl_cursor_image_get_buffer(image) возвращает raw wl_buffer *. Этот буфер принадлежит wl_cursor (внутри темы). Мы не должны его освобождать.

Мы его и не освобождаем — у нас AttachRaw (не создаёт TWlBuffer). ОК.

Но: в TWLCursor.SetImage мы вызываем:
pascal

FSurface.AttachRaw(BufRaw, 0, 0);
FSurface.Damage(...);
FSurface.Commit;

Surface attach'ит буфер. Когда wl_buffer уничтожается (wl_cursor_theme_destroy), а surface ещё привязана к нему — libwayland может решить, что surface тоже невалидна.

Точнее: wl_surface ещё до сих пор в pending состоянии, и при попытке wl_surface_destroy libwayland проверяет буфер — а он уже уничтожен.

Порядок должен быть:

    Сначала отвязать буфер от surface: wl_surface.attach(nil).

    Потом освободить wl_cursor_theme (уничтожит буферы).

    Потом освободить wl_surface курсора.

    Потом освободить wl_pointer.

У нас порядок:

    FCursor.Free → освобождает surface (с привязанным буфером) — падает здесь.

    FCursorTheme.Free → не доходит.

Решение

Переделать TWLCursor.Destroy — сначала отвязать буфер:
pascal

destructor TWLCursor.Destroy;
begin
  if FSurface <> nil then
  begin
    // Отвязываем буфер перед освобождением surface
    FSurface.AttachRaw(nil, 0, 0);
    FSurface.Commit;
    if FDisplay <> nil then
      FDisplay.Flush;   // нужно поле Display

    FreeAndNil(FSurface);
  end;
  inherited;
end;

Но: FSurface.Commit на поверхность, привязанную к wl_pointer.set_cursor — тоже может быть проблема. Композитор держит surface в курсоре. Если мы отвяжем буфер — курсор исчезнет. Это нормально при завершении.

Хуже: wl_pointer.set_cursor(..., nil, ...) — правильнее сначала скрыть курсор, потом уничтожать.
Правильный порядок закрытия

Переделать TWLEventManager.Destroy:
pascal

destructor TWLEventManager.Destroy;
begin
  // 1. Скрываем курсор у композитора
  if (FCursor <> nil) and (FPointer <> nil) then
    FPointer.SetCursor(FLastEnterSerial, nil, 0, 0);   // ← новое

  // 2. Освобождаем surface курсора
  if FCursor <> nil then FreeAndNil(FCursor);

  // 3. Освобождаем тему (уничтожит wl_cursor и wl_buffer)
  if FCursorTheme <> nil then FreeAndNil(FCursorTheme);

  // 4. Потом pointer и keyboard
  FPointerListener := nil;
  FKeyboardListener := nil;
  FSeatListener := nil;
  if FPointer <> nil then FreeAndNil(FPointer);
  if FKeyboard <> nil then FreeAndNil(FKeyboard);
  inherited;
end;

Ключевое — шаг 1: wl_pointer.set_cursor(nil) — композитор отвязывает surface от курсора.

Но: wl_pointer.set_cursor принимает serial. Если FLastEnterSerial — устаревший serial, композитор проигнорирует. Значит, не гарантируется, что курсор отвяжется.

Второй вариант: не уничтожать surface курсора вообще. Утечка при завершении — не критична (процесс всё равно заканчивается).

Простейший фикс:
pascal

destructor TWLCursor.Destroy;
begin
  // Не освобождаем surface курсора. libwayland всё равно освободит
  // все ресурсы при wl_display_disconnect.
  FSurface := nil;   // ← просто забываем про неё
  inherited;
end;

Ещё проще — в TWLEventManager.Destroy освободить FCursor без освобождения surface:

Заменить в TWLEventManager.Destroy:
pascal

// Было:
if FCursor <> nil then FreeAndNil(FCursor);

// Стало: пропускаем освобождение cursor surface
FCursor := nil;   // ← утечка, но безопасно

Мой рекомендуемый фикс

Изменить TWLCursor.Destroy — не освобождать поверхность:
pascal

destructor TWLCursor.Destroy;
begin
  // Surface курсора НЕ освобождаем.
  // Причина: surface привязана к wl_pointer.set_cursor, и при
  // wl_surface_destroy libwayland делает wl_proxy_destroy поверх
  // буфера, который к этому моменту уже мог быть освобождён
  // wl_cursor_theme_destroy. Это приводит к SIGSEGV.
  //
  // Утечка одной surface при завершении процесса — не критична:
  // wl_display_disconnect всё равно освободит ресурсы.
  FSurface := nil;
  inherited;
end;

Плюс — изменить TWLEventManager.Destroy, чтобы сначала освободить FCursor, потом FCursorTheme:
pascal

destructor TWLEventManager.Destroy;
begin
  if FCursor <> nil then FreeAndNil(FCursor);           // FSurface не освободится
  if FCursorTheme <> nil then FreeAndNil(FCursorTheme); // освободит wl_cursor и wl_buffer

  FPointerListener := nil;
  FKeyboardListener := nil;
  FSeatListener := nil;
  if FPointer <> nil then FreeAndNil(FPointer);
  if FKeyboard <> nil then FreeAndNil(FKeyboard);
  inherited;
end;

Это безопасно: wl_cursor_theme_destroy освободит буферы. Наша wl_surface никогда не будет освобождена явно — при wl_display_disconnect libwayland сама подчистит.
Что делать

    Патч wlgui_cursor.pas — TWLCursor.Destroy не освобождает FSurface (просто FSurface := nil).

    Патч wlgui_events.pas — в Destroy порядок: FCursor → FCursorTheme → FPointer/FKeyboard.

    Собрать, запустить — при выходе не должно падать.

Проверка: запусти demo, нажми Ctrl+C. Если раньше падало при выходе — теперь не должно. И клик по item не должен падать (падение было при выходе, а клик — просто доходил до конца программы? Или падало при клике?).

Перечитал: в логе падение не при клике, а при завершении программы. #17 main () at demo_listbox.pas:220 — последняя строка (после MyApp.Free). Ты нажал Ctrl+C, demo вышел, при освобождении MyApp — упало.

Значит проблема только при выходе. Клик работал.
Патч

wlgui_cursor.pas — замени TWLCursor.Destroy:
pascal

destructor TWLCursor.Destroy;
begin
  // Surface курсора НЕ освобождаем (см. комментарий в коде).
  // Это предотвращает SIGSEGV в wl_proxy_destroy при завершении.
  FSurface := nil;
  inherited;
end;

wlgui_events.pas — если порядок не такой, исправь на:
pascal

destructor TWLEventManager.Destroy;
begin
  if FCursor <> nil then FreeAndNil(FCursor);
  if FCursorTheme <> nil then FreeAndNil(FCursorTheme);
  FPointerListener := nil;
  FKeyboardListener := nil;
  FSeatListener := nil;
  if FPointer <> nil then FreeAndNil(FPointer);
  if FKeyboard <> nil then FreeAndNil(FKeyboard);
  inherited;
end;

Собери, запусти, закрой. Не должно падать.
Итог

    Не наша вина — известная проблема libwayland-cursor: wl_cursor_theme_destroy освобождает буферы, а наша surface всё ещё ссылается на них.

    Решение: не освобождать surface курсора явно. libwayland подчистит при disconnect.

    Утечка одной surface при завершении — нормально и безопасно.

Пришли результат — если при выходе больше не падает, продолжим дальше (u4builder, многострочный Edit, или что-то ещё).
Starting program: /tmp/demo_listbox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
wlgui listbox demo
[wlgui] EventManager created
[xkb] libxkbcommon loaded
[xkb] context created
[wlgui] Wayland initialized
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=20 valid=TRUE builtin=FALSE ascent=19 descent=5 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=14 valid=TRUE builtin=FALSE ascent=13 descent=3 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=16 valid=TRUE builtin=FALSE ascent=15 descent=4 units_per_EM=2048
[wlgui] window created: "wlgui listbox demo" 640x480
[events] seat name: default
[events] seat capabilities=3
[cursor] libwayland-cursor loaded
[cursor] theme loaded: "" size=24
[cursor] cursor surface created
[events] pointer attached
[events] keyboard attached
[wlgui] 2 buffers created: 640x480 stride=2560
[events] keyboard keymap format=1 size=38030
[xkb] keymap loaded

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
(gdb) bt full
#0  0x00007ffff7f5bf98 in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#1  0x00007ffff7f55a6e in ?? () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#2  0x00007ffff7f57c92 in wl_proxy_marshal_array_flags () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#3  0x00007ffff7f58272 in wl_proxy_marshal_flags () from /usr/lib/x86_64-linux-gnu/libwayland-client.so.0
No symbol table info available.
#4  0x00007ffff7a6a91d in wl_cursor_theme_destroy () from /usr/lib/x86_64-linux-gnu/libwayland-cursor.so.0
No symbol table info available.
#5  0x000055555565b1f5 in DESTROY (this=0x7ffff7f9c840, vmt=0x1) at wlgui_cursor.pas:222
No locals.
#6  0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#7  0x0000555555600e17 in SYSUTILS_$$_FREEANDNIL$formal ()
No symbol table info available.
#8  0x00005555556368b2 in DESTROY (this=0x7ffff7f75040, vmt=0x1) at wlgui_events.pas:642
No locals.
#9  0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#10 0x000055555562dfa8 in DESTROY (this=0x7ffff7f8d1c0, vmt=0x1) at wlgui_context.pas:57
No locals.
#11 0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#12 0x0000555555600e17 in SYSUTILS_$$_FREEANDNIL$formal ()
No symbol table info available.
#13 0x000055555562e91a in DESTROY (this=0x7ffff7f9c280, vmt=0x1) at wlgui_app.pas:139
        I = 0
#14 0x00005555555c1cfb in SYSTEM$_$TOBJECT_$__$$_FREE ()
No symbol table info available.
#15 0x00005555555b039a in main () at demo_listbox.pas:220
No locals.
🎯 Теперь падает в wl_cursor_theme_destroy — та же болезнь, следующий уровень

Смотрим стек:
text

#5  TWLCursorTheme.Destroy (wlgui_cursor.pas:222)  ← wl_cursor_theme_destroy
#4  wl_cursor_theme_destroy
#3  wl_proxy_marshal_flags
#2  wl_proxy_marshal_array_flags
#0  SIGSEGV

Теперь падает wl_cursor_theme_destroy. Мы освободили FCursor (не освобождая surface), потом — FCursorTheme — и это падает.
Почему

wl_cursor_theme_destroy вызывает wl_proxy_marshal_flags — отправляет запрос композитору (destroy для wl_shm_pool внутри темы, или для wl_buffer, или... что-то). Падает внутри marshal.

Причина: wl_display уже невалиден. Смотрим порядок в wlgui_app.pas:139:
pascal

destructor TWLApplication.Destroy;
var
  I: Integer;
begin
  // ... закрываем окна ...
  Finalize;   ← ← ЗДЕСЬ
  if FOwnsContext and (FContext <> nil) then
    FreeAndNil(FContext);   ← ← или здесь
  inherited;
end;

А в Finalize (строка ~148):
pascal

procedure TWLApplication.Finalize;
begin
  if FContext.Subcompositor <> nil then FreeAndNil(FContext.Subcompositor);
  if FContext.WmBase <> nil then FreeAndNil(FContext.WmBase);
  if FContext.Seat <> nil then FreeAndNil(FContext.Seat);
  if FContext.Shm <> nil then FreeAndNil(FContext.Shm);
  if FContext.Compositor <> nil then FreeAndNil(FContext.Compositor);
  if FRegistry <> nil then FreeAndNil(FRegistry);
  if FDisplay <> nil then
  begin
    FDisplay.Disconnect;   ← ← ← ВОТ ОНО! DISCONNECT РАНЬШЕ, ЧЕМ EVENTS
    FreeAndNil(FDisplay);
    FContext.Display := nil;
  end;
end;

FDisplay.Disconnect вызывается в Finalize. А TWLEventManager (внутри FContext.Events) освобождается в TWLContext.Destroy ПОЗЖЕ. И TWLCursorTheme.Destroy пытается отправить запрос через уже отключённый wl_display.

Вот в чём дело!
Правильный порядок закрытия

Сначала освобождаем все прокси-объекты (они могут захотеть что-то отправить), потом — Disconnect.

Текущий порядок (неправильный):
text

1. FContext.Subcompositor.Free
2. FContext.WmBase.Free
3. FContext.Seat.Free
4. FContext.Shm.Free
5. FContext.Compositor.Free
6. FRegistry.Free
7. FDisplay.Disconnect         ← Отключаем
8. FreeAndNil(FContext)        ← Освобождаем Events → CursorTheme → ПАДАЕТ

Правильный порядок:
text

1. Сначала — Events (внутри Context) — включая курсор, темы, pointer, keyboard, seat listener
2. Потом — Subcompositor, WmBase, Seat, Shm, Compositor
3. Потом — Registry
4. Потом — Display.Disconnect

Решение

Переделать TWLApplication.Destroy — освободить Events ДО Finalize:
pascal

destructor TWLApplication.Destroy;
var
  I: Integer;
begin
  if FContext <> nil then
  begin
    // 1. Закрываем окна
    for I := FContext.Windows.Count - 1 downto 0 do
    begin
      try
        TObject(FContext.Windows[I]).Free;
      except
      end;
    end;

    // 2. Освобождаем Events ДО Finalize (дисплей ещё жив)
    if FContext.Events <> nil then
      FreeAndNil(FContext.Events);
  end;

  // 3. Только теперь Finalize — он отключит display
  Finalize;

  if FOwnsContext and (FContext <> nil) then
    FreeAndNil(FContext);

  inherited;
end;

Плюс: убрать освобождение Events из TWLContext.Destroy — оно уже сделано раньше:
pascal

destructor TWLContext.Destroy;
begin
  // Events уже освобождён в TWLApplication.Destroy
  if Events <> nil then
    FreeAndNil(Events);   // ← на всякий случай (но будет nil)
  FWindows.Free;
  inherited;
end;

Оставь — FreeAndNil(nil) безопасен.
Патч wlgui_app.pas

Замени TWLApplication.Destroy:
pascal

destructor TWLApplication.Destroy;
var
  I: Integer;
begin
  if FContext <> nil then
  begin
    // 1. Сначала закрываем окна
    for I := FContext.Windows.Count - 1 downto 0 do
    begin
      try
        TObject(FContext.Windows[I]).Free;
      except
      end;
    end;

    // 2. Освобождаем EventManager (курсор, pointer, keyboard, seat listener)
    //    ПОКА display ещё жив — иначе cursor_theme_destroy упадёт
    if FContext.Events <> nil then
      FreeAndNil(FContext.Events);
  end;

  // 3. Теперь можно отключать display
  Finalize;

  if FOwnsContext and (FContext <> nil) then
    FreeAndNil(FContext);

  inherited;
end;

Что ещё важно проверить

TWLContext.Destroy — если Events там тоже освобождается, то двойное освобождение. Смотри wlgui_context.pas:57 в стеке — это TWLContext.Destroy, где вызывается FWindows.Free или Events.Free.

Проверь:
pascal

destructor TWLContext.Destroy;
begin
  if Events <> nil then
  begin
    Events.Free;      ← ← Если это здесь, а мы уже освободили в App — двойное!
    Events := nil;
  end;
  FWindows.Free;
  inherited;
end;

Должно быть так:
pascal

destructor TWLContext.Destroy;
begin
  if Events <> nil then
    FreeAndNil(Events);   // ← безопасно, если Events = nil
  if FWindows <> nil then
    FWindows.Free;
  inherited;
end;

FreeAndNil проверяет на nil. Если в App уже FreeAndNil(FContext.Events) — здесь Events = nil, и FreeAndNil ничего не делает.

Но: если в App не освободили, то здесь — освободим. Порядок гарантирован.
Итог

Причина падения: FDisplay.Disconnect до освобождения Events. TWLCursorTheme.Destroy вызывает wl_cursor_theme_destroy, который отправляет запрос через уже отключённый display → SIGSEGV.

Фикс: освободить Events до Finalize (пока FDisplay ещё подключён).
Патч

wlgui_app.pas — переделать TWLApplication.Destroy (код выше).

wlgui_context.pas — убедиться, что TWLContext.Destroy безопасен при Events = nil.

wlgui_cursor.pas — вернуть освобождение FSurface?

Стоп. Мы отключили освобождение surface — теперь курсор не освобождается. Это привело к следующей ошибке? Нет — ошибка не в surface, а в theme. wl_cursor_theme_destroy падает по другой причине (display уже disconnected).

Значит: надо вернуть освобождение surface? Или нет?

Проверка: если wl_cursor_theme_destroy падает из-за disconnected display, то если освободить theme до disconnect — оно пройдёт. А FCursor (с surface) — освобождается до FCursorTheme в нашем EventManager. При этом само wl_surface_destroy тоже отправляет запрос — и тоже должно быть до disconnect.

Значит: всё освобождение Wayland-объектов должно быть до disconnect. Мы сделали это через FreeAndNil(FContext.Events) до Finalize.

И: вернуть освобождение FSurface в TWLCursor.Destroy? Или оставить FSurface := nil?

Тест: с новым порядком (Events.Free до Disconnect) — попробуй вернуть FreeAndNil(FSurface). Если не падает — оставляем. Если падает — возвращаемся к FSurface := nil.

Начни с:

    Патч TWLApplication.Destroy — освободить Events до Finalize.

    Оставить TWLCursor.Destroy как есть (FSurface := nil).

Собери, запусти, закрой (Ctrl+C или закрытие окна). Проверь, падает ли.

Пришли результат.
Starting program: /tmp/demo_listbox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
wlgui listbox demo
[wlgui] EventManager created
[xkb] libxkbcommon loaded
[xkb] context created
[wlgui] Wayland initialized
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=20 valid=TRUE builtin=FALSE ascent=19 descent=5 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=14 valid=TRUE builtin=FALSE ascent=13 descent=3 units_per_EM=2048
[font] sizeof(FT_FaceRec)=160 offset(glyph)=152 offset(units_per_EM)=136 offset(ascender)=138
[font] file=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf name=DejaVuSans height=16 valid=TRUE builtin=FALSE ascent=15 descent=4 units_per_EM=2048
[wlgui] window created: "wlgui listbox demo" 640x480
[events] seat name: default
[events] seat capabilities=3
[cursor] libwayland-cursor loaded
[cursor] theme loaded: "" size=24
[cursor] cursor surface created
[events] pointer attached
[events] keyboard attached
[wlgui] 2 buffers created: 640x480 stride=2560
[events] keyboard keymap format=1 size=38030
[xkb] keymap loaded
An unhandled exception occurred at $00005555555E57FD:
EConvertError: Invalid argument index in format "Selected #%d: "%s""
  $00005555555E57FD
  $00005555555E6693
  $00005555555E6BA0
  $00005555555AF568  UPDATESTATUS,  line 30 of demo_listbox.pas
  $00005555555AF63E  ONLISTCHANGE,  line 38 of demo_listbox.pas
  $0000555555638CC4  SETITEMINDEX,  line 144 of wlgui_listbox.pas
  $0000555555639275  DOMOUSEUP,  line 272 of wlgui_listbox.pas
  $000055555563772F  DISPATCHMOUSEUP,  line 262 of wlgui_widget.pas
  $0000555555637705  DISPATCHMOUSEUP,  line 258 of wlgui_widget.pas
  $00005555555AFC00  ONMOUSEUP,  line 103 of demo_listbox.pas
  $0000555555630E49  WLRECVMOUSEUP,  line 617 of wlgui_window.pas
  $0000555555635CB1  WL_POINTER_BUTTON,  line 423 of wlgui_events.pas
  $0000555555656F49  WL_POINTER_BUTTON_INTF,  line 1812 of wayland_protocol.pas
  $00007FFFF7C7B056

[Inferior 1 (process 47536) exited with code 0331]
(gdb) bt full
❌ No stack.
🎯 Падение при выходе исчезло! Осталась ошибка Format

Стек говорит:
text

EConvertError: Invalid argument index in format "Selected #%d: "%s""
  UPDATESTATUS, line 30 of demo_listbox.pas

Format('%s', [IU4String]) — FPC не знает, как обработать интерфейс IU4String. Это та же проблема, о которой я предупреждал.
Что значит «Invalid argument index»

Format('Selected #%d: "%s"', [List.ItemIndex, List.Items[List.ItemIndex]]):

    %d ← List.ItemIndex — Integer ✅

    %s ← List.Items[List.ItemIndex] — IU4String ❌

FPC для %s ожидает String/PChar. IU4String не подходит. Компилятор не проверяет типы в array of const — падает в runtime.
Патч demo_listbox.pas

В uses — добавь u4utf8:
pascal

uses
  cthreads, SysUtils, Classes,
  u4intf, u4utf8, u4wrap,        // ← добавить
  wlgui_types, wlgui_context, wlgui_app, wlgui_window, wlgui_canvas, wlgui_font,
  wlgui_events, wlgui_widget, wlgui_button, wlgui_label,
  wlgui_listbox;

Замени UpdateStatus:
pascal

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

Замени OnListDblClick:
pascal

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

Общее правило

FPC Format не поддерживает IU4String. Везде, где IU4String попадает в:

    Format

    WriteLn

    + (конкатенацию) со String

    Caption := ... (напрямую)

— нужен явный U4ToUTF8(...) или .ToUTF8 (через u4wrap).
Дополнительно: утилита для будущего

Создадим в u4helper.pas функцию:
pascal

function U4ToStr(const S: IU4String): String; inline;
begin
  Result := U4ToUTF8(S);
end;

Тогда в коде — U4ToStr(List.Items[i]) — коротко и понятно.

Или используй helper из u4wrap:
pascal

uses u4wrap;
...
List.Items[List.ItemIndex].ToUTF8   // ← тоже работает

Оба варианта хороши. Для читаемости — .ToUTF8 через helper.
Патч + сборка

Патчим demo, собираем:
bash

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

Теперь должно работать:

    Клик по item — выделение, статус обновляется.

    Ctrl+C — чистый выход без SIGSEGV.

Что мы исправили по пути (важно на будущее)

    Порядок освобождения Wayland-объектов: Events → Finalize (disconnect).

    wl_cursor_theme_destroy до Disconnect — иначе SIGSEGV.

    TWLCursor.Destroy не освобождает surface — оставлена на disconnect (иначе другой SIGSEGV).

    Format с IU4String — нельзя, нужен U4ToUTF8.

Сохрани эти знания — они неочевидны и легко забыть.
Итог

Причина нового падения: Format('%s', [IU4String]).
Фикс: U4ToUTF8 перед Format.

Патчим demo, пересобираем. Если при выходе больше не падает и клик работает — продолжаем.
Что дальше

После успешного запуска — предлагаю:

    u4builder.pas — TU4StringBuilder для эффективной сборки строк. Пригодится в логах, редакторах, шаблонах.

    u4lines.pas — TU4Lines для многострочного текста (будущий многострочный Edit).

    Скроллбар для ListBox — визуальный индикатор.

    u4helper.pas — расширить утилитами (U4ToStr, U4ReplaceAll, U4Trim).

Мой план: начнём с u4builder — самый полезный. Он понадобится для логов, будущего редактора событий, шаблонов уведомлений.

Пришли результат запуска — и выбирай, куда двигаемся.