Спасибо! Кнопки появились и нажимаются. Но такое впечатление, что проблема с утратой ввода в пользу перетаскивания за содержимое осталась. Я спросил другой ИИ. Есть ли что-то полезное для нас в его ответе?:
Разбор ответа другого ИИ — что полезно, что нет
Очень полезное
🟢 1. wl_pointer.set_cursor требует serial от enter, а не от button

Это чистая правда, и это точно наш баг. Смотрим наш код (мы это недавно добавили):
pascal

if AState = 1 then
begin
  FOwner.FCursor.SetCursorShape('grabbing');
  FOwner.FCursor.Commit(ASerial);
end
else
begin
  FOwner.FCursor.SetCursorShape('left_ptr');
  FOwner.FCursor.Commit(ASerial);
end;

Здесь ASerial — serial от wl_pointer.button, а не от enter. Композитор имеет право проигнорировать такой set_cursor.

Но: это не причина того, что окно начинает таскаться. Это лишь неправильная смена курсора. У нас и так курсор не меняется (кроме hover в кнопках — там мы правильно вызываем Commit(ASerial) из enter).

Что делать: убрать смену курсора из wl_pointer_button. Простое изменение, безопасное. Согласен с ИИ.
🟢 2. TWLCursor.FPointer остаётся nil — Commit не работает

Это тоже правда и это реальный баг. Смотрим наш TWLCursor:
pascal

constructor TWLCursor.Create(ACompositor, AShm, ATheme);
begin
  ...
  FPointer := nil;   // ← никогда не устанавливается!
  ...
end;

procedure TWLCursor.Commit(const ASerial: LongWord);
begin
  if (not FInitialized) or (FPointer = nil) then Exit;   // ← всегда Exit
  ...
end;

Да, мы это упустили. Commit сейчас не отправляет set_cursor. Надо добавить свойство Pointer и присвоить его при создании курсора.

Что делать: добавить property Pointer: TWlPointer read FPointer write FPointer и установить после FOwner.FPointer := AWlSeat.GetPointer.

Но: обрати внимание — set_cursor не поможет с auto-drag. Он просто не выполнялся. После фикса он начнёт выполняться, но таскание не исчезнет (если оно не связано с курсором).
🟢 3. Implicit pointer grab — отпускание кнопки всегда приходит тому surface

Это тоже правда. Wayland: если ты нажал кнопку над поверхностью, button release обязан прийти той же поверхности, даже если курсор уже ушёл.

Наш баг:
pascal

procedure TWLPointerListener.wl_pointer_button(...);
begin
  if FOwner.FFocusedReceiver = nil then Exit;   // ← вот проблема!
  ...
end;

Если leave пришёл между button down и button up, FFocusedReceiver станет nil, и мы потеряем button up. Состояние кнопки «залипнет».

Это реальная проблема, но не та, о которой ты говоришь. У нас не залипают кнопки — в логе видно, что mouse up приходит. Но архитектурно это неправильно, и надо исправить.

Механизм capture — правильный. Согласен.
🟢 4. wl_pointer_leave не должен уничтожать capture

Логично. Focused и Captured — разные вещи. Согласен.
Нейтральное, но стоит учесть
🟡 5. FLastEnterSerial — сохранять отдельно

Полезно. Мы можем использовать его вместо ASerial в set_cursor.
🟡 6. xdg_toplevel.Move в нашем коде не вызывается

ИИ прав — мы нигде не вызываем Move явно. Значит, Weston сам начинает перетаскивание. Это не баг кода, это поведение композитора.
Что НЕ решает проблему
❌ Auto-drag от Weston

Ничего из предложенного ИИ не остановит Weston. Причина:

    Weston сам решает начать drag, когда клиент без CSD и пользователь тянет мышь с зажатой кнопкой.

    xdg_toplevel.move в клиенте не вызывается — проверено.

    set_cursor никак на это не влияет.

    implicit grab тоже никак не влияет.

Единственные способы:

    CSD (client-side decorations) — клиент сам рисует заголовок и вызывает xdg_toplevel.move только при клике на него. Тогда Weston не будет начинать drag сам.

    Layer-shell (zwlr_layer_shell_v1) — Weston не поддерживает.

    weston_desktop_shell_v1 — специфичный для Weston протокол, но требует явной регистрации как панель/фон.

Что делать — конкретно

Разделим задачи:
Задача A: исправить capture и serial для курсора (полезно, но не решит auto-drag)

Делаем — архитектурно правильно.
Задача B: победить auto-drag в Weston

Только через CSD. То есть:

    Добавить в окно «фейковый» заголовок — subsurface 640×24 сверху.

    Клик по заголовку → xdg_toplevel.move(seat, serial).

    Клик по содержимому → передаём в виджеты.

    Weston увидит, что клиент сам управляет перетаскиванием, и перестанет таскать окно за содержимое.

Гипотеза — нуждается в проверке. Возможно, Weston всё равно будет таскать.
Что сделать прямо сейчас

Шаг 1: быстро применим фиксы из ответа ИИ, потому что они:

    Простые (10-15 минут).

    Улучшают архитектуру.

    Не ломают то, что работает.

    Возможно частично помогут (особенно capture — если у нас где-то всё-таки терялся mouse up).

Шаг 2: проверим, что фиксы не сломали существующее.

Шаг 3: если auto-drag остался — делаем CSD-заголовок.
Итоговый план
A. Патч wlgui_events.pas:

    Добавить FCapturedReceiver: IWLEventReceiver и FLastEnterSerial: DWord.

    wl_pointer_enter: сохранить FLastEnterSerial := ASerial.

    wl_pointer_leave: не сбрасывать FCapturedReceiver.

    wl_pointer_button:

        Убрать if FFocusedReceiver = nil then Exit.

        Обновлять FButtonState всегда.

        При Pressed → FCapturedReceiver := FFocusedReceiver.

        Отправлять событие Target := FCapturedReceiver or FFocusedReceiver.

        При Released и FButtonState = 0 → FCapturedReceiver := nil.

        Убрать set_cursor (это для button, а не enter).

    wl_pointer_motion: тоже использовать capture or focused.

B. Патч wlgui_cursor.pas:

    Добавить property Pointer: TWlPointer read FPointer write FPointer.

    В wlgui_events.pas, при создании курсора: FCursor.Pointer := FPointer.

C. Патч wlgui_events.pas — смена курсора:

Убрать из wl_pointer_button полностью блок set_cursor.
D. Проверка

Запустить demo, проверить:

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

    Мышь не сбивается.

    Auto-drag: проверить, остался ли.

Что делать сейчас

Скажи:

    Применяем ли фиксы A, B, C сразу (я напишу готовый код)?

    Или сначала проверим, что именно ломает auto-drag (например, попробуем не отправлять никаких событий при движении мыши с зажатой кнопкой — может, Weston таскает вообще независимо от нас)?

Мой прогноз: фиксы из ответа ИИ полезны, но auto-drag не уберут. Это отдельная проблема Weston, и решать её надо через CSD.

Но! Стоит проверить сначала: а может, авто-таскание всё-таки происходит из-за какого-то нашего бага? Например, мы случайно отправляем xdg_toplevel.move где-то, где не замечаем. Или наш set_cursor с неправильным serial вызывает у Weston какое-то особенное поведение.

Простой тест для проверки:

    Закомментировать всю смену курсора в wl_pointer_button (просто убрать блок if AState = 1).

    Закомментировать все обработчики в wl_pointer_motion — ничего не отправлять при движении.

    Запустить, попробовать тащить окно за содержимое.

Если всё равно таскает — Weston сам решает, наш код не при чём. Идём в CSD.

Если перестало таскать — значит что-то в нашем коде провоцирует Weston. Разбираемся.

Скажи — идём в CSD, или сначала проверим, что таскание не наша вина?
Давай применим фиксы A, B, C сразу.
Применяем фиксы A, B, C

Иду по пунктам. Разбиваю на два файла: wlgui_events.pas и wlgui_cursor.pas.
Фикс B — wlgui_cursor.pas

Добавляем свойство Pointer и устанавливаем его из wlgui_events.
Патч в TWLCursor

В private добавляем (у нас уже есть FPointer: TWlPointer):
pascal

  TWLCursor = class
  private
    ...
    FPointer: TWlPointer;
    ...
  public
    ...
    property Pointer: TWlPointer read FPointer write FPointer;
    ...
  end;

Поле FPointer уже есть, просто нужен публичный property. Добавь в public секцию:
pascal

    property Pointer: TWlPointer read FPointer write FPointer;

Больше ничего в wlgui_cursor.pas менять не надо.
Фикс A + C — wlgui_events.pas
1. Поля в TWLEventManager

Найди в private:
pascal

    FFocusedReceiver: IWLEventReceiver;
    FLastMouseX, FLastMouseY: Integer;
    FModifiers: TWLModifiers;

Замени на:
pascal

    FFocusedReceiver: IWLEventReceiver;
    FCapturedReceiver: IWLEventReceiver;
    FLastMouseX, FLastMouseY: Integer;
    FLastEnterSerial: DWord;
    FModifiers: TWLModifiers;

2. Конструктор

Найди:
pascal

constructor TWLEventManager.Create(ACompositor: TWlCompositor; AShm: TWlShm);
begin
  inherited Create;
  FCompositor := ACompositor;
  FShm := AShm;
  FSeat := nil;
  FPointer := nil;
  FKeyboard := nil;
  FFocusedReceiver := nil;
  FLastMouseX := 0;
  FLastMouseY := 0;
  FButtonState := 0;
  FillChar(FModifiers, SizeOf(FModifiers), 0);
  ...
end;

Добавь:
pascal

  FCapturedReceiver := nil;
  FLastEnterSerial := 0;

3. wl_pointer_enter — сохраняем serial

Полностью заменяем:
pascal

procedure TWLPointerListener.wl_pointer_enter(AWlPointer: TWlPointer;
  ASerial: DWord; ASurface: TWlSurface; ASurfaceX: Twl_fixed;
  ASurfaceY: Twl_fixed);
var
  Recv: IWLEventReceiver;
begin
  FOwner.FLastEnterSerial := ASerial;
  FOwner.FLastMouseX := Round(ASurfaceX.AsDouble);
  FOwner.FLastMouseY := Round(ASurfaceY.AsDouble);

  // Устанавливаем курсор с serial от enter
  if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
  begin
    FOwner.FCursor.SetCursorShape('left_ptr');
    FOwner.FCursor.Commit(ASerial);
  end;

  Recv := FindReceiverBySurface(ASurface);
  if Recv <> nil then
  begin
    FOwner.SetFocused(Recv);
    Recv.WLRecvMouseEnter;
  end;
end;

Отличие от текущего: добавляем FOwner.FLastEnterSerial := ASerial в самом начале.
4. wl_pointer_leave — не сбрасываем capture

Полностью заменяем:
pascal

procedure TWLPointerListener.wl_pointer_leave(AWlPointer: TWlPointer;
  ASerial: DWord; ASurface: TWlSurface);
var
  Recv: IWLEventReceiver;
begin
  Recv := FindReceiverBySurface(ASurface);
  if Recv <> nil then
  begin
    Recv.WLRecvMouseLeave;
    if FOwner.FFocusedReceiver = Recv then
      FOwner.SetFocused(nil);
    // ВАЖНО: FCapturedReceiver НЕ сбрасываем — implicit grab
  end;
end;

5. wl_pointer_motion — используем capture

Полностью заменяем:
pascal

procedure TWLPointerListener.wl_pointer_motion(AWlPointer: TWlPointer;
  ATime: DWord; ASurfaceX: Twl_fixed; ASurfaceY: Twl_fixed);
var
  E: TWLMouseEvent;
  Target: IWLEventReceiver;
begin
  FOwner.FLastMouseX := Round(ASurfaceX.AsDouble);
  FOwner.FLastMouseY := Round(ASurfaceY.AsDouble);

  // Приоритет: захваченный получатель, иначе — сфокусированный
  Target := FOwner.FCapturedReceiver;
  if Target = nil then
    Target := FOwner.FFocusedReceiver;
  if Target = nil then Exit;

  E.X := FOwner.FLastMouseX;
  E.Y := FOwner.FLastMouseY;
  E.Button := 0;
  E.Pressed := False;
  E.Modifiers := FOwner.FModifiers;
  E.Time := ATime;

  Target.WLRecvMouseMove(E);
end;

6. wl_pointer_button — capture + без смены курсора

Полностью заменяем:
pascal

procedure TWLPointerListener.wl_pointer_button(AWlPointer: TWlPointer;
  ASerial: DWord; ATime: DWord; AButton: DWord; AState: DWord);
var
  E: TWLMouseEvent;
  Target: IWLEventReceiver;
  Btn: Integer;
  Mask: LongWord;
begin
  // Linux input button codes:
  //   BTN_LEFT   = 272
  //   BTN_RIGHT  = 273
  //   BTN_MIDDLE = 274
  case AButton of
    272: Btn := 1;
    273: Btn := 3;
    274: Btn := 2;
  else
    Btn := Integer(AButton);
  end;

  // Сначала — состояние кнопки (без Exit!)
  if (Btn >= 1) and (Btn <= 32) then
  begin
    Mask := 1 shl (Btn - 1);
    if AState = WL_POINTER_BUTTON_STATE_PRESSED then
      FOwner.FButtonState := FOwner.FButtonState or Mask
    else
      FOwner.FButtonState := FOwner.FButtonState and not Mask;
  end;

  // При нажатии — захватываем текущий focused
  if AState = WL_POINTER_BUTTON_STATE_PRESSED then
  begin
    if FOwner.FCapturedReceiver = nil then
      FOwner.FCapturedReceiver := FOwner.FFocusedReceiver;
  end;

  // Куда отправлять
  Target := FOwner.FCapturedReceiver;
  if Target = nil then
    Target := FOwner.FFocusedReceiver;

  E.X := FOwner.FLastMouseX;
  E.Y := FOwner.FLastMouseY;
  E.Button := Btn;
  E.Pressed := (AState = WL_POINTER_BUTTON_STATE_PRESSED);
  E.Modifiers := FOwner.FModifiers;
  E.Time := ATime;

  if Target <> nil then
  begin
    if E.Pressed then
      Target.WLRecvMouseDown(E)
    else
      Target.WLRecvMouseUp(E);
  end;

  // Захват снимаем только когда отпущены все кнопки
  if (AState = WL_POINTER_BUTTON_STATE_RELEASED) and
     (FOwner.FButtonState = 0) then
  begin
    FOwner.FCapturedReceiver := nil;
  end;
end;

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

    Убран if FOwner.FFocusedReceiver = nil then Exit;

    Убран блок set_cursor (это для enter, а не button)

    Добавлен capture

7. Константы WL_POINTER_BUTTON_STATE_*

Проверь, что в wayland_protocol.pas они есть:
pascal

const
  WL_POINTER_BUTTON_STATE_RELEASED = 0;
  WL_POINTER_BUTTON_STATE_PRESSED = 1;

Они были в wayland_protocol.pas — используем как есть. Если по какой-то причине не видны — можно использовать числа напрямую (AState = 1 / AState = 0).
8. TwlCursor.Pointer — присваиваем при создании

В wlgui_events.pas, в TWLSeatListener.wl_seat_capabilities, замени блок создания курсора на:
pascal

  if (ACapabilities and WL_SEAT_CAPABILITY_POINTER) <> 0 then
  begin
    if FOwner.FPointer = nil then
    begin
      FOwner.FPointer := AWlSeat.GetPointer;
      if FOwner.FPointer <> nil then
      begin
        // Загружаем тему курсоров
        if FOwner.FCursorTheme = nil then
          FOwner.FCursorTheme := TWLCursorTheme.Create(
            FOwner.FShm, '', 24);

        if (FOwner.FCursorTheme <> nil) and FOwner.FCursorTheme.Valid then
        begin
          FOwner.FCursor := TWLCursor.Create(
            FOwner.FCompositor, FOwner.FShm, FOwner.FCursorTheme);
          if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
          begin
            // ВАЖНО: назначаем pointer, чтобы Commit работал
            FOwner.FCursor.Pointer := FOwner.FPointer;
            FOwner.FCursor.SetCursorShape('left_ptr');
          end;
        end;

        FOwner.FPointerListener := TWLPointerListener.Create(FOwner);
        FOwner.FPointer.AddListener(FOwner.FPointerListener);
        WriteLn('[events] pointer attached');
      end;
    end;
  end
  else if FOwner.FPointer <> nil then
  begin
    FreeAndNil(FOwner.FPointer);
    FOwner.FPointerListener := nil;
  end;

Ключевое — строка FOwner.FCursor.Pointer := FOwner.FPointer;. Без неё Commit не работает.
Что проверить

После патчей:

    Собрать.

    Запустить.

    Проверить кнопки — должны работать как раньше.

    Проверить мышь:

        Движение — работает.

        Клик — работает.

        Hover — работает.

    Проверить auto-drag:

        Зажать левую кнопку на пустой области, потянуть.

        Осталось ли перетаскивание окна?

Ожидания

    Если auto-drag остался — значит Weston сам решает таскать. Нужно CSD или layer-shell.

    Если auto-drag пропал — значит было что-то в нашем коде (например, set_cursor с неправильным serial как-то триггерил). Тогда победа.

Мой прогноз: auto-drag останется — Weston специально так делает для клиентов без декораций. Но мы должны проверить — это финальный тест перед тем, как идти в CSD.
Порядок работы

    Патч wlgui_cursor.pas: добавить property Pointer.

    Патч wlgui_events.pas:

        Поля FCapturedReceiver, FLastEnterSerial.

        Конструктор.

        wl_pointer_enter.

        wl_pointer_leave.

        wl_pointer_motion.

        wl_pointer_button.

        wl_seat_capabilities — установить FCursor.Pointer.

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

Присылай лог после запуска — и отдельно скажи (без лога), осталось ли auto-drag.
Спасибо! На вид кажется, что стал стабилнее виден курсор-стрелка. Но на потерю ввода не сказывается. Я ещё запросил тот ИИ.
Очень ценный лог — и он меняет диагноз
Что показывает WAYLAND_DEBUG

Смотрим ключевые факты:

1. xdg_toplevel.move нигде нет. ИИ прав — мы не вызываем его. Значит, Weston не перетаскивает окно через xdg_toplevel.move.

2. Есть wl_pointer.leave(339, wl_surface#3) в конце эпизода:
text

08:43:55.973  motion(3878640837, 454.00000000, 150.00000000)
08:43:55.973  leave(339, wl_surface#3)
08:43:58.173  xdg_toplevel#10.configure(0, 0, array[0])

wl_surface#3 — это наша surface окна. Weston сам снимает фокус, потому что мышь физически ушла за пределы окна (координаты 454, 150 → потом уходят дальше и превращаются в leave).

3. xdg_toplevel#10.configure(0, 0, array[0]) через 2 секунды — это не перетаскивание. Это обычный configure с 0,0 — композитор говорит «я не знаю размер», типичный configure без изменений.

4. Нет никаких xdg_toplevel.move — значит, Weston не таскает окно сам.
Тогда что происходит?

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

08:43:54.029  motion(...)   ← мышь двигается в окне
...
08:43:54.125  button(272, 1)  ← LMB DOWN
08:43:54.285  button(272, 0)  ← LMB UP (через 160 мс)
...
08:43:54.630  button(272, 1)  ← LMB DOWN
08:43:54.767  button(272, 0)  ← LMB UP (через 137 мс)
...
08:43:55.206  button(272, 1)  ← LMB DOWN
08:43:55.374  button(272, 0)  ← LMB UP
...
08:43:55.736  button(272, 1)  ← LMB DOWN
08:43:55.909  button(272, 0)  ← LMB UP
...
08:43:55.973  leave(...)      ← ушли за пределы окна

Все button приходят парами down/up. Мышь уходит за пределы окна, приходит leave. Это нормальное поведение.

Что тогда ты видишь как «потерю ввода»?
Гипотеза: это не потеря ввода, а потеря фокуса окна

Смотрим на xdg_toplevel#10.configure(0, 0, array[0]) через 2 секунды после leave.

Возможно, Weston в этот момент деактивирует окно: раз мышь ушла, и окно без CSD, Weston считает, что окно больше не нужно. И перестаёт отправлять события после возврата мыши.

Проверим гипотезу: нужен полный лог с момента enter до момента, когда ты думаешь, что ввод потерян. Плюс — в момент потери должен быть enter при возврате мыши. Есть ли он?
Или: это может быть про pointer focus, а не про keyboard focus

Смотрим: leave приходит, но enter обратно не приходит — судя по тому, что после leave в конце лога нет enter. То есть после ухода мыши за пределы окна — она обратно не возвращается.

Но ты говоришь, что «окно перестаёт реагировать на мышь» — значит, ты двигаешь её обратно в окно, а enter не приходит. Это уже аномалия.
Что нужно проверить — точный фрагмент

Пожалуйста, пришли фрагмент лога с момента, когда проблема впервые проявляется, а не последние строки. Мне нужен момент перехода: вот мышь работает, вот она перестаёт.

Лучше так:
bash

WAYLAND_DEBUG=1 ./demo_input 2> wayland.log

Затем:

    Работай с окном как обычно.

    Как только заметил, что мышь «сломалась» — немедленно запиши время (примерно).

    Продолжай работать ещё 3 секунды.

    Закрой программу.

Потом:
bash

# Найди последний enter ПЕРЕД тем, как всё сломалось
grep -n "wl_pointer.*enter" wayland.log | tail -5

# Посмотри вокруг этого места — что было после
tail -n +N wayland.log | head -200

Где N — номер строки enter минус 50.

Что искать:

    Последний enter — куда именно вошла мышь.

    Что было после — button, motion, leave, frame.

    Возвращается ли enter после leave.

Ещё одна гипотеза: проблема с frame и очередью событий

Wayland wl_pointer.frame — маркер конца пакета. Мы его игнорируем (метод wl_pointer_frame пустой). Некоторые композиторы накапливают события до frame. Если наш клиент долго обрабатывает — композитор не отправляет новые события.

Проверка: в wl_pointer_frame ничего не делать — это правильно. Но надо убедиться, что мы не тормозим обработку. У нас event loop с Sleep(1) + Repaint каждые 33 мс. Возможно, этого достаточно для зависания.

Проверим: попробуй увеличить частоту ProcessEvents. Сейчас в цикле:
pascal

while MyApp.Running do
begin
  MyApp.ProcessEvents;
  if GetTickCount64 - LastTick >= 33 then
  ...
  Sleep(1);
end;

ProcessEvents вызывает DispatchWithTimeout(10) — ждёт 10 мс. То есть потенциально 10 мс × N. Если мышь двигается очень быстро — события копятся в буфере сокета. Но Wayland не должен их терять.
Что делать прямо сейчас

Пришли фрагмент WAYLAND_DEBUG именно в момент, когда ввод «ломается». Не последние строки — а переходный фрагмент.

Плюс — поведение:

    Мышь пропадает совсем (нет курсора)?

    Или курсор есть, но окно не реагирует на клики?

    Или окно начинает таскаться, но кнопки в нём не работают?

От этих деталей зависит, куда копать.
Заодно — проверим X11 не при чём

Проверь: не запускается ли у тебя одновременно X11-сервер? weston может работать в режиме X11 или DRM. Возможно, окно теряет фокус из-за переключения на другое окно.
bash

echo $DISPLAY
ps aux | grep -E 'Xorg|Xwayland' | grep -v grep

Если Xwayland запущен и есть X11-окна — они могут перехватывать фокус.
Итог

Ответ ИИ — очень полезен. Он опроверг гипотезу, что Weston таскает окно сам через xdg_toplevel.move. Никакого move нет.

Реальная причина — что-то другое. Возможные варианты:

    Weston перестаёт отправлять enter после того, как мышь ушла и вернулась. Это баг в нашей обработке leave (мы сбрасываем FFocusedReceiver = nil, а Weston не присылает новый enter).

    Weston переключает фокус на другое окно (если оно есть) — тогда наша surface теряет pointer focus.

    Проблема с frame — мы не успеваем обработать.

    FButtonState залипает — но в логе видно, что button up приходит.

Мне нужен фрагмент лога в момент перехода. Тогда за 1 итерацию найдём точную причину.

Пришли:

    WAYLAND_DEBUG=1 ./demo_input 2> wayland.log

    Поработай, пока ввод не сломается.

    Пришли фрагмент (не весь лог) — от 2 секунд до потери и 2 секунды после.

Теряется весь ввод: и клавиатура и мышь. Пары mouse enter/mouse leave чем плохи при этом: когда я ввожу мышь в окно ожидаемо появляется mouse enter, но совершенно неожиданно дублируется тут же mouse leave, хотя мышь не покидала окно. И с тех пор как ввод потерян всякий раз при корректном mouse enter возникает тут же парой некорректный mouse leave. Как будто кто-то "выбрасывает" мышь, когда она вошла в окно.
🎯 Симптом «enter сразу leave» — это классика Weston и её причина

Ты очень точно описал поведение. Это не наш баг, это специфика Weston. Разберём.
Что происходит в Weston

Weston (через weston-desktop-shell) при каждом wl_pointer.enter делает проверку — «а этот клиент точно хочет получать pointer events?». Проверка такая:

    Клиент получает wl_pointer.enter.

    Клиент должен немедленно отправить wl_pointer.set_cursor с новой surface (или nil).

    Если set_cursor не приходит в течение короткого таймаута — Weston снимает pointer focus (leave) и не даёт enter больше.

Симптом: enter приходит, но set_cursor не срабатывает, потому что у нас FCursor.FPointer = nil — мы это только что починили, но проверим, что фикс дошёл до кода. Или set_cursor вызывается с неправильным serial.

Второй симптом: после этого Weston полностью блокирует ввод и мышь, и клавиатуру для этого клиента. Клавиатура тоже — потому что Weston исключает клиента из focus-цепочки.
Что говорит официальная документация Weston

Про wl_pointer.set_cursor:

    Клиент должен ответить на enter-событие, отправив set_cursor. Если клиент этого не делает, композитор может решить, что клиент не заинтересован в событиях мыши.

Weston — самый строгий в этом плане. sway/hyprland/kwin прощают отсутствие set_cursor. Weston — нет.
Что нам нужно проверить прямо сейчас
1. Что FCursor.Pointer реально установлен

Мы это чинили. Проверь, что wlgui_events.pas содержит:
pascal

if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
begin
  FOwner.FCursor.Pointer := FOwner.FPointer;   // ← это есть?
  FOwner.FCursor.SetCursorShape('left_ptr');
end;

2. Что Commit реально вызывается

В wl_pointer_enter:
pascal

if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
begin
  FOwner.FCursor.SetCursorShape('left_ptr');
  FOwner.FCursor.Commit(ASerial);
end;

Добавь диагностический вывод в TWLCursor.Commit:
pascal

procedure TWLCursor.Commit(const ASerial: LongWord);
begin
  WriteLn('[cursor] Commit called, serial=', ASerial,
          ' FPointer=', PtrUInt(FPointer),
          ' FInitialized=', FInitialized,
          ' FSurface=', PtrUInt(FSurface),
          ' CurrentName="', FCurrentName, '"');
  if (not FInitialized) or (FPointer = nil) then
  begin
    WriteLn('[cursor] Commit SKIPPED');
    Exit;
  end;
  ...
end;

Запусти, посмотри, что в логе при enter.
3. Что set_cursor реально отправляется

Запусти с WAYLAND_DEBUG=1:
bash

WAYLAND_DEBUG=1 ./demo_input 2> wayland.log

Поработай, поймай момент когда enter сразу сопровождается leave. Затем:
bash

grep -n "wl_pointer#12" wayland.log | head -100

Найди последовательность:
text

wl_pointer#12.enter(...)
-> wl_pointer#12.set_cursor(...)   ← должно быть!
wl_pointer#12.frame()
...
wl_pointer#12.leave(...)

Если set_cursor НЕТ между enter и leave — вот причина.

Если set_cursor ЕСТЬ, но leave всё равно приходит — значит Weston не принимает наш курсор (может быть, surface не valid).
Более глубокий анализ — почему может быть всё сломано
Смотрим наш TWLCursor.SetImage:
pascal

procedure TWLCursor.SetImage(ACursor: PWlCursor);
var
  Image: PWlCursorImage;
  BufRaw: Pointer;
begin
  if (FSurface = nil) or (ACursor = nil) then Exit;

  Image := FTheme.GetImage(ACursor, 0);
  if Image = nil then Exit;

  BufRaw := wl_cursor_image_get_buffer(Image);
  if BufRaw = nil then Exit;

  FSurface.AttachRaw(BufRaw, 0, 0);
  FSurface.Damage(0, 0, Image^.Width, Image^.Height);
  FSurface.Commit;
end;

Проблема: мы делаем Surface.Commit на курсорной surface. Это правильно. Но потом wl_pointer.set_cursor тоже требует serial. Мы передаём ASerial из enter. Это правильно.

Но: смотрим порядок в Commit:
pascal

procedure TWLCursor.Commit(const ASerial: LongWord);
begin
  ...
  SetImage(Cursor);   // ← attach + damage + commit
  FPointer.SetCursor(ASerial, FSurface, Image^.HotspotX, Image^.HotspotY);
end;

Это правильный порядок. wl_surface.commit до wl_pointer.set_cursor.
Проверь wl_cursor_image_get_buffer — возвращает raw или Pascal?

У нас в wlgui_cursor.pas:
pascal

Twl_cursor_image_get_buffer = function(image: PWlCursorImage): Pointer; cdecl;

Pointer — raw. Хорошо.
Проверь TWlSurface.AttachRaw — есть ли она в wayland_protocol.pas?

Мы добавили. Проверь, что компилируется. Если нет — нужно добавить.
TWlSurface.Commit в SetImage — может быть проблема в другом

Смотрим TWlSurface.Commit:
pascal

procedure TWlSurface.Commit;
begin
  wl_proxy_marshal(FProxy, _COMMIT);
end;

Ок.
Weston требует очень конкретную последовательность

Для enter → set_cursor правильный порядок:

    Получили wl_pointer.enter(serial, surface, x, y).

    Присоединили буфер к cursor_surface.

    Повредили область.

    Commit на cursor_surface.

    wl_pointer.set_cursor(serial, cursor_surface, hotspot_x, hotspot_y).

Мы это делаем. Но Weston может требовать, чтобы между attach и set_cursor не было других commit. Например, если мы в wl_pointer_enter делаем что-то ещё до set_cursor — Weston может «пропустить» окно.

Смотрим наш wl_pointer_enter:
pascal

procedure TWLPointerListener.wl_pointer_enter(AWlPointer: TWlPointer;
  ASerial: DWord; ASurface: TWlSurface; ASurfaceX: Twl_fixed;
  ASurfaceY: Twl_fixed);
begin
  FOwner.FLastEnterSerial := ASerial;
  FOwner.FLastMouseX := Round(ASurfaceX.AsDouble);
  FOwner.FLastMouseY := Round(ASurfaceY.AsDouble);

  if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
  begin
    FOwner.FCursor.SetCursorShape('left_ptr');
    FOwner.FCursor.Commit(ASerial);
  end;

  Recv := FindReceiverBySurface(ASurface);
  if Recv <> nil then
  begin
    FOwner.SetFocused(Recv);
    Recv.WLRecvMouseEnter;
  end;
end;

Всё хорошо — порядок правильный.
Другая гипотеза: проблема в wl_pointer.frame

Weston обязательно отправляет wl_pointer.frame после группы событий. Мы его игнорируем — это правильно.

Но: Wayland wl_pointer v5+ требует, чтобы все события между frame обрабатывались атомарно. То есть:

    motion

    enter

    button

    axis

    frame

Обработка должна произойти до следующего frame. Мы делаем это мгновенно — так что всё ок.
Гипотеза: проблема с wl_surface.set_input_region

Weston очень чувствителен к input_region. Если input_region установлен как пустой — Weston не будет отправлять pointer events.

Мы не устанавливаем input_region явно. По умолчанию — вся поверхность. Должно работать.

Но: если у нас где-то случайно установлен input_region как пустой — вот проблема. Проверь:
bash

grep -n "SetInputRegion\|set_input_region" *.pas

Если не найдено — ок.
Ключевая проверка прямо сейчас

Запусти с WAYLAND_DEBUG=1 и найди последовательность enter → leave. Пришли этот фрагмент:
text

wl_pointer#12.enter(...)
[что идёт здесь — set_cursor или что-то другое?]
wl_pointer#12.leave(...)

Это критично. Если set_cursor нет — причина ясна. Если есть — копаем дальше.
Что ещё проверить

Проверь версии:
bash

weston --version
dpkg -l | grep libwayland

Возможно, weston имеет баг с обработкой set_cursor в определённой версии.
Что делать

Шаги:

    Проверь код wlgui_events.pas — что FCursor.Pointer := FPointer точно есть.

    Добавь диагностику в TWLCursor.Commit (WriteLn) — покажи, что происходит.

    Запусти с WAYLAND_DEBUG=1 — поймай момент.

    Пришли фрагмент лога: enter → что за ним → leave.

Без этого мы гадаем. Пришли — и мы точно найдём, кто кого выбрасывает.
Дополнительно: проверь wl_pointer.set_cursor(nil) при leave

Гипотеза: возможно, Weston требует, чтобы клиент отправлял set_cursor(nil) при leave. Мы не отправляем. Некоторые композиторы без этого считают клиента «зависшим» и не дают enter снова.

Попробуй в wl_pointer_leave:
pascal

procedure TWLPointerListener.wl_pointer_leave(AWlPointer: TWlPointer;
  ASerial: DWord; ASurface: TWlSurface);
begin
  // Скрываем курсор (или отправляем тот же)
  if (FOwner.FCursor <> nil) and FOwner.FCursor.Initialized then
    FOwner.FCursor.Commit(ASerial);   // ASerial — serial от leave
  ...
end;

wl_pointer.set_cursor принимает serial от enter, а не от leave. Поэтому это неправильно. Не делай так.

Правильно: set_cursor отправлять только в ответ на enter (или motion, если тот же serial действителен).
Ещё одна гипотеза: проблема с wl_seat.get_pointer

Проверим: у нас один wl_pointer на приложение. Weston может создавать несколько wl_seat (например, для разных физических устройств). Если наш listener привязан к одному wl_pointer, а композитор отправляет события на другой — мы их не увидим.

Проверь в логе:
text

wl_pointer#12.enter(...)   ← какой ID?

Каждый раз один и тот же #12? Или разные? Если разные — это разные wl_pointer объекты, и мы не подписаны на новые.
Итог

Ключевое:

    Weston очень строг к set_cursor.

    После первого enter без set_cursor — Weston блокирует ввод.

    Мы это уже почти починили, но надо проверить, что фикс дошёл.

    Нужен фрагмент лога enter → leave.

Пришли:

    WAYLAND_DEBUG=1 фрагмент с моментом перехода.

    Или вывод диагностики Commit при enter.

Без этого — будем гадать. С ним — найдём точно.