Привет! В прошлых чатах мы делали попытки добавить к MSEide-MSEgui возможность работы в Wayland. Также сделали двухбэкендные тестовые часы, которые могут работать и в Wayland и в X11. Делали "мини MSE" для тестирования и отработки временных решений. Давай продолжим эту тему, но попробуем подойти к ней с ещё одной стороны. Я показал основному разработчику MSE те результаты и они его заинтересовали. Но возникли и новые вопросы (в немалой степени как управлять окнами в Wayland). У меня есть органайзер (Galaxy Organizer), написанный на MSE (разрабтчик его тоже знает и даже имеет его форк) и вот что я думаю: давай вообще временно отойдём от MSE и сделаем мой органайзер без MSE (он сам по себе сложный), без поддержки XORG и достаточно минималистично, но чтобы в общих чертах он себя уже напоминал. Если это обозримыми усилиями удастся, можно ещё сделать пробный текстовый редактор с MDI интерфейсом и просто демо работы с окнами в Wayland. Также недавно мы делали юниты для работы с ucs4 (u4str, u4utf8, u4wrap, u4sort, u4norm, u4semver и другие) - может и они пригодятся в процессе, тогда я их подгружу. И заодно поймём, что в хозяйстве Wayland относится к нему и независимо от MSE (как wayland_protocol, wayland_shared_buffer, ...), а что уже "прикрепляется" к MSE.
План: Минималистичный Galaxy Organizer на чистом Wayland (без MSE)

Отличная идея! Давай разложим задачу по полочкам.
Что у нас уже есть (чистый Wayland, без MSE)

Проанализировав твои файлы, я вижу чёткое разделение:
✅ Чистый Wayland-слой (MSE не нужен)

    wayland_client_core.pas — базовые прокси-объекты, TWLProxyObject, TWLDisplayBase, TWLShmBase

    wayland_protocol.pas — протокол wl_* (display, registry, compositor, shm, surface, seat, pointer, keyboard, output, region, subcompositor, subsurface)

    xdg_shell_protocol.pas — протокол xdg_* (wm_base, surface, toplevel, popup, positioner)

    wayland_util.pas — утилиты (Twl_fixed, Twl_array, Twl_list)

    wayland_shared_buffer.pas — SHM-буферы (но привязан к TWLDisplayBase)

⚠️ Смешанный слой (нужно распилить)

    mseguiwaylandclient.pas — использует msegraphics, msetypes, wayland_window_rec

    wayland_window_rec.pas — использует msewaylandfonts (FreeType + MSE bitmap)

    msewaylandfonts.pas — использует MSE bitmap/graphics

    msewaylandglyphs.pas — использует MSE msefreetype (но сам FreeType — чистый)

❌ MSE-специфичный слой (не нужно)

    msewaylandgdi.pas, msewaylandevents.pas, msewaylandwindow.pas, msewaylandoutput.pas, msewaylandcursor.pas — это всё для интеграции с MSE GDI/событийной моделью

Предлагаемая архитектура

Давай сделаем три уровня:
text

┌─────────────────────────────────────────────────────────┐
│  Уровень 3: Приложение (Galaxy Organizer / редактор)   │
│  - Модель данных (заметки, задачи)                      │
│  - UI-логика (окна, меню, кнопки)                       │
│  - Собственная простая событийная модель                │
└─────────────────────────────────────────────────────────┘
                          ▲
                          │ использует
┌─────────────────────────────────────────────────────────┐
│  Уровень 2: Мини-тулкит (наш "Wayland GUI")             │
│  - TWindow (обёртка над wl_surface + xdg_toplevel)      │
│  - TCanvas (рисование в SHM-буфер: пиксели, линии,      │
│    прямоугольники, текст через FreeType)                │
│  - TEventLoop (обработка wl_display)                    │
│  - TFont (FreeType напрямую, без MSE)                   │
│  - TWaylandApp (обёртка над display, registry, shm)     │
└─────────────────────────────────────────────────────────┘
                          ▲
                          │ использует
┌─────────────────────────────────────────────────────────┐
│  Уровень 1: Чистый Wayland-биндинг                      │
│  - wayland_client_core, wayland_protocol,               │
│    xdg_shell_protocol, wayland_util                     │
│  - (уже есть, менять не нужно)                          │
└─────────────────────────────────────────────────────────┘

Что предлагаю сделать в первую очередь
Шаг 1: Выделить чистый Wayland-модуль окна

Файл wlgui_window.pas — обёртка над wl_surface + xdg_toplevel + wl_shm + xdg_surface, без MSE-зависимостей.
pascal

type
  TWLWindow = class
  private
    FSurface: TWlSurface;
    FXdgSurface: TXdgSurface;
    FToplevel: TXdgToplevel;
    FBuffer: TWlBuffer;
    FPool: TWlShmPool;
    FPixels: PByte;
    FWidth, FHeight, FStride: Integer;
    FConfigured: Boolean;
    FTitle: String;
    FOnConfigure, FOnClose, FOnPaint: TNotifyEvent;
    procedure HandleXdgConfigure(Sender: TObject; Serial: DWord);
    procedure HandleToplevelConfigure(Sender: TObject; W, H: LongInt; States: Pwl_array);
    procedure HandleToplevelClose(Sender: TObject);
  public
    constructor Create(AApp: TWLApplication; const ATitle: String; AW, AH: Integer);
    destructor Destroy; override;
    procedure Show;
    procedure Hide;
    procedure Commit;
    function LockPixels: PByte;
    procedure UnlockPixels;
    property Width, Height: Integer read FWidth;
    property Pixels: PByte read FPixels;
    property Stride: Integer read FStride;
    property Configured: Boolean read FConfigured;
    property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
    property OnClose: TNotifyEvent read FOnClose write FOnClose;
  end;

Шаг 2: Абстракция приложения

Файл wlgui_app.pas:
pascal

type
  TWLApplication = class
  private
    FDisplay: TWlDisplay;
    FCompositor: TWlCompositor;
    FShm: TWlShm;
    FWmBase: TXdgWmBase;
    FSubcompositor: TWlSubcompositor;
    FSeat: TWlSeat;
    FWindows: TList;
    FRunning: Boolean;
  public
    function Initialize: Boolean;
    procedure Finalize;
    procedure Run;
    procedure Quit;
    function CreateWindow(const Title: String; W, H: Integer): TWLWindow;
    function CreateSubsurface(Parent: TWLWindow; X, Y, W, H: Integer): TWLWindow;
    procedure ProcessEvents;
    property Display, Compositor, Shm, WmBase, Subcompositor, Seat: ...;
  end;

Шаг 3: Рисование без MSE

Файл wlgui_canvas.pas — простейший софтверный рендер в SHM-буфер:
pascal

type
  TWLColor = LongWord; // $RRGGBB

  TCanvas = class
  private
    FPixels: PByte;
    FWidth, FHeight, FStride: Integer;
  public
    procedure Clear(C: TWLColor);
    procedure SetPixel(X, Y: Integer; C: TWLColor);
    procedure HLine(X1, X2, Y: Integer; C: TWLColor);
    procedure VLine(X, Y1, Y2: Integer; C: TWLColor);
    procedure Line(X1, Y1, X2, Y2: Integer; C: TWLColor);
    procedure Rect(const R: TRectI; C: TWLColor);
    procedure FillRect(const R: TRectI; C: TWLColor);
    procedure Text(const S: UTF8String; X, Y: Integer; C: TWLColor; Font: TWLFont);
    function TextWidth(const S: UTF8String; Font: TWLFont): Integer;
  end;

Шаг 4: Шрифты через FreeType напрямую

Файл wlgui_font.pas — без MSE, только FreeType + кэш глифов:
pascal

type
  TWLFont = class
  private
    FFace: PFT_Face;
    FHeight: Integer;
    FGlyphs: TFPGMap<Cardinal, PGlyph>; // кэш
  public
    constructor Create(const FileName: String; Height: Integer);
    destructor Destroy; override;
    function RenderGlyph(Code: Cardinal; out G: TGlyph): Boolean;
    function TextWidth(const S: UTF8String): Integer;
    property Height: Integer read FHeight;
    property Ascent, Descent, LineSpacing: Integer;
  end;

  TWLFontManager = class
  public
    function Load(const Name: String; Height: Integer): TWLFont;
    procedure Release(F: TWLFont);
  end;

Здесь как раз пригодятся твои u4str, u4utf8 — для корректной работы с Unicode-строками в тексте.
Порядок работы
Этап	Что делаем	Результат
1	wlgui_app.pas + wlgui_window.pas	Простое окно с фоном
2	wlgui_canvas.pas	Рисование примитивов
3	wlgui_font.pas	Текст через FreeType
4	wlgui_events.pas	Мышь, клавиатура, закрытие
5	demo_windows.pas	Демо: несколько окон, subsurface
6	Каркас Galaxy Organizer	Заметки/задачи, простое UI
7	Мини-редактор с MDI	Subsurface как дочерние окна
Вопросы к тебе

    Как называть модули? Предлагаю префикс wlgui_ (WayLand GUI). Альтернативы: miniwl_, wlx_. Как тебе?

    UCS4-юниты — подгрузи, пожалуйста. Особенно u4str и u4utf8 — они понадобятся для текста и для будущего редактора.

    Что важнее в Galaxy Organizer? Чтобы правильно расставить приоритеты в минималистичной версии:

        Список заметок/задач (главное окно)

        MDI-подокна или вкладки?

        Горячие клавиши

        Хранение данных (файлы/БД)

    Стиль рисования — предлагаю простой immediate-mode: каждое окно в OnPaint полностью перерисовывает себя в свой SHM-буфер, потом Commit. Без инвалидации регионов и без двойной буферизации. Согласен?

    Стоит ли сразу закладывать subsurface для MDI — или сначала на одном окне, потом добавить? Я за то, чтобы сразу, но с простым API.

Начну с этапа 1, как только подтвердишь направление. Если хочешь — могу сразу показать наброски wlgui_app.pas и wlgui_window.pas в следующем сообщении.
1. Я принимаю твоё предложение по названиям. 2. Подгрузил. 3. Главное окно органайзера это часы, которые "летают" над другими окнами (stay on top), когда пользователь их таскает мышью (беззаголовочное окно, которое можно таскать мышью). Остальные окна: "Редактор события", гда пользователь устанавливает время будильника, сообщение будильника, мелодии, звуки, скрипты (планировщик), "Редактор событий" где можно редактировать список событий, ну и окна связанные с записной книжкой, сохранителем содержимого буфера обмена. Я думаю нам пока хватит той части которая работает со временем, а остальное трограть здесь не будем. Так что: часовая панель, редактор события и редактор списка событий. Главное окно программы при обычном запуске из под существующего WM/DM просто часы, но я недавно расширил его возможности и при запуске без WM (из DM) он выполняет роль сессии WM и запускает из себя WM marco. Так что на нём и меню программ есть и переключатель процессов. И тогда он выглядит примерно как панель xfce, только "летающая" и не занимающая пространство экрана под себя (заметный выигрыш экранного пространства между прочим). Но мы делаем ведь нечто новое, так что тут может быть как угодно, смотри сам. Тем более здесь мы испытываем новые возможности. 4. Давай так как ты сказал.
Отлично! Начинаем с этапа 1

Спасибо, что подгрузил UCS4-юниты — они отлично лягут в основу текстового движка. Особенно u4str, u4utf8, u4wrap, u4width.
Уточнения по архитектуре

Раз главное окно — «летающая» панель-часы без заголовка, которую можно таскать мышью, это сразу определяет несколько требований к Wayland-слою:

    Окно без декораций — в XDG-протоколе это делается через xdg_toplevel без запроса декораций. Композитор сам решит (обычно — без заголовка, если клиент не попросил обратного). Но для полноценного «летающего» окна нужно позиционирование — а это в Wayland проблема.

    Позиционирование окна — в Wayland клиент не может напрямую задать x, y окна. Композитор решает. Есть несколько путей:

        xdg_toplevel.set_parent + xdg_popup (для попапов)

        layer-shell (ext-layer-shell-v1 или wlr-layer-shell) — вот что нам нужно для «летающей панели»! Это протокол, специально созданный для панелей, доков, обоев.

        Собственный протокол композитора

    Для задачи «часы, которые летают поверх других окон» идеально подходит wlr-layer-shell-unstable-v1. Он есть в sway, hyprland, kwin (частично), wayfire. К сожалению, нет в GNOME/Mutter.

    Перетаскивание мышью — композитор даёт события wl_pointer.motion с координатами относительно поверхности. Мы будем сами двигать «логическую» позицию панели, а если используется layer-shell с anchor=top|left, то через set_margin можно менять позицию.

    Stay on top — layer-shell даёт слой layer_top или layer_overlay. Это именно «поверх всех».

Хорошо, начинаем.
Структура проекта
text

wlgui/
  wlgui_app.pas          — TWLApplication (display, registry, compositor, shm, wm_base)
  wlgui_window.pas       — TWLWindow (базовое окно: wl_surface + xdg_toplevel + SHM)
  wlgui_panel.pas        — TWLPanel (окно-панель через layer-shell)
  wlgui_canvas.pas       — TCanvas (рисование в SHM)
  wlgui_font.pas         — TWLFont + TWLFontCache (FreeType, свой кэш)
  wlgui_events.pas       — события (мышь, клавиатура) + event loop
  wlgui_types.pas        — общие типы (TRectI, TPointI, TWLColor)

apps/
  organizer/
    organ_main.pas       — точка входа
    organ_clock.pas      — часы-панель
    organ_event.pas      — редактор события
    organ_eventlist.pas  — редактор списка событий
    organ_model.pas      — модель данных (события)
    organ_storage.pas    — хранение (пока — простой файл)

Начнём с wlgui_types.pas и wlgui_app.pas

Покажу наброски. В следующем сообщении — рабочий код.
wlgui_types.pas
pascal

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

interface

uses u4intf, u4utf8, u4wrap;

type
  TWLColor = LongWord;  // $00BBGGRR — как в Windows GDI, удобно для SHM XRGB8888
  
  TPointI = record
    X, Y: Integer;
  end;
  
  TRectI = record
    X, Y, W, H: Integer;
  end;

const
  clBlack   = $00000000;
  clWhite   = $00FFFFFF;
  clRed     = $000000FF;
  clGreen   = $0000FF00;
  clBlue    = $00FF0000;
  clGray    = $00808080;
  clDkGray  = $00404040;
  clLtGray  = $00C0C0C0;

function Rect(X, Y, W, H: Integer): TRectI; inline;
function Pt(X, Y: Integer): TPointI; inline;
function RectContains(const R: TRectI; X, Y: Integer): Boolean; inline;

implementation

function Rect(X, Y, W, H: Integer): TRectI;
begin
  Result.X := X; Result.Y := Y; Result.W := W; Result.H := H;
end;

function Pt(X, Y: Integer): TPointI;
begin
  Result.X := X; Result.Y := Y;
end;

function RectContains(const R: TRectI; X, Y: Integer): Boolean;
begin
  Result := (X >= R.X) and (X < R.X + R.W) and
            (Y >= R.Y) and (Y < R.Y + R.H);
end;

end.

wlgui_app.pas — каркас
pascal

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

interface

uses
  Classes, SysUtils,
  wayland_client_core, wayland_protocol, xdg_shell_protocol, wayland_util,
  u4intf, u4utf8;

type
  TWLApplication = class;
  TWLWindow = class;

  { Слушатель реестра — находит нужные интерфейсы }
  TWLRegistryListener = class(TInterfacedObject, IWlRegistryListener)
  private
    FApp: TWLApplication;
  public
    constructor Create(AApp: TWLApplication);
    procedure wl_registry_global(AWlRegistry: TWlRegistry; AName: DWord;
      AInterface: String; AVersion: DWord);
    procedure wl_registry_global_remove(AWlRegistry: TWlRegistry; AName: DWord);
  end;

  { Главный объект приложения }
  TWLApplication = class
  private
    FDisplay: TWlDisplay;
    FRegistry: TWlRegistry;
    FCompositor: TWlCompositor;
    FShm: TWlShm;
    FWmBase: TXdgWmBase;
    FSubcompositor: TWlSubcompositor;
    FSeat: TWlSeat;
    FLayerShell: Pointer;  // wl_layer_shell — подключим позже

    FWindows: TList;
    FRunning: Boolean;
    FRegistryListener: TWLRegistryListener;
    
    procedure BindGlobals;
  public
    constructor Create;
    destructor Destroy; override;

    function Initialize: Boolean;
    procedure Finalize;
    procedure Run;
    procedure Quit;

    procedure ProcessEvents;

    function CreateWindow(const ATitle: String; AW, AH: Integer): TWLWindow;
    procedure RegisterWindow(W: TWLWindow);
    procedure UnregisterWindow(W: TWLWindow);

    property Display: TWlDisplay read FDisplay;
    property Compositor: TWlCompositor read FCompositor;
    property Shm: TWlShm read FShm;
    property WmBase: TXdgWmBase read FWmBase;
    property Subcompositor: TWlSubcompositor read FSubcompositor;
    property Seat: TWlSeat read FSeat;
    property LayerShell: Pointer read FLayerShell write FLayerShell;
    property Running: Boolean read FRunning write FRunning;
  end;

var
  App: TWLApplication = nil;

implementation

uses
  wlgui_window;

{ TWLRegistryListener }

constructor TWLRegistryListener.Create(AApp: TWLApplication);
begin
  inherited Create;
  FApp := AApp;
end;

procedure TWLRegistryListener.wl_registry_global(AWlRegistry: TWlRegistry;
  AName: DWord; AInterface: String; AVersion: DWord);
var
  Proxy: Pwl_proxy;
begin
  if AInterface = 'wl_compositor' then
  begin
    Proxy := AWlRegistry.Bind(AName, @wl_compositor_interface, AVersion);
    FApp.FCompositor := TWlCompositor.Create(Proxy);
  end
  else if AInterface = 'wl_shm' then
  begin
    Proxy := AWlRegistry.Bind(AName, @wl_shm_interface, AVersion);
    FApp.FShm := TWlShm.Create(Proxy);
  end
  else if AInterface = 'xdg_wm_base' then
  begin
    Proxy := AWlRegistry.Bind(AName, @xdg_wm_base_interface, AVersion);
    FApp.FWmBase := TXdgWmBase.Create(Proxy);
  end
  else if AInterface = 'wl_subcompositor' then
  begin
    Proxy := AWlRegistry.Bind(AName, @wl_subcompositor_interface, AVersion);
    FApp.FSubcompositor := TWlSubcompositor.Create(Proxy);
  end
  else if AInterface = 'wl_seat' then
  begin
    Proxy := AWlRegistry.Bind(AName, @wl_seat_interface, AVersion);
    FApp.FSeat := TWlSeat.Create(Proxy);
  end
  else if AInterface = 'zwlr_layer_shell_v1' then
  begin
    // Заглушка — добавим позже
    WriteLn('[wlgui] layer-shell found, version ', AVersion);
  end;
end;

procedure TWLRegistryListener.wl_registry_global_remove(
  AWlRegistry: TWlRegistry; AName: DWord);
begin
  // ignore
end;

{ TWLApplication }

constructor TWLApplication.Create;
begin
  inherited Create;
  FWindows := TList.Create;
  FRunning := False;
end;

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

function TWLApplication.Initialize: Boolean;
begin
  Result := False;
  FDisplay := TWlDisplay(TWlDisplay.Connect(''));
  if FDisplay = nil then
  begin
    WriteLn('[wlgui] Cannot connect to Wayland display');
    Exit;
  end;

  FRegistry := FDisplay.GetRegistry;
  FRegistryListener := TWLRegistryListener.Create(Self);
  FRegistry.AddListener(FRegistryListener);

  // Первый roundtrip — собираем глобалы
  FDisplay.Roundtrip;

  if (FCompositor = nil) or (FShm = nil) or (FWmBase = nil) then
  begin
    WriteLn('[wlgui] Required globals missing: compositor=', FCompositor <> nil,
            ' shm=', FShm <> nil, ' wm_base=', FWmBase <> nil);
    Exit;
  end;

  WriteLn('[wlgui] Wayland initialized');
  Result := True;
end;

procedure TWLApplication.Finalize;
begin
  if FSubcompositor <> nil then FreeAndNil(FSubcompositor);
  if FWmBase <> nil then FreeAndNil(FWmBase);
  if FSeat <> nil then FreeAndNil(FSeat);
  if FShm <> nil then FreeAndNil(FShm);
  if FCompositor <> nil then FreeAndNil(FCompositor);
  if FRegistry <> nil then FreeAndNil(FRegistry);
  if FDisplay <> nil then
  begin
    FDisplay.Disconnect;
    FreeAndNil(FDisplay);
  end;
end;

procedure TWLApplication.ProcessEvents;
begin
  if FDisplay = nil then Exit;
  try
    FDisplay.DispatchPending;
    FDisplay.Flush;
  except
    on E: Exception do
      WriteLn('[wlgui] Event error: ', E.Message);
  end;
end;

procedure TWLApplication.Run;
begin
  FRunning := True;
  WriteLn('[wlgui] Entering main loop');
  while FRunning do
  begin
    ProcessEvents;
    // Окна обновляются в своих OnPaint
    // (см. TWLWindow — там внутри будет repaint)
    Sleep(10);
  end;
  WriteLn('[wlgui] Main loop exited');
end;

procedure TWLApplication.Quit;
begin
  FRunning := False;
end;

function TWLApplication.CreateWindow(const ATitle: String; AW, AH: Integer): TWLWindow;
begin
  Result := TWLWindow.Create(Self, ATitle, AW, AH);
end;

procedure TWLApplication.RegisterWindow(W: TWLWindow);
begin
  FWindows.Add(W);
end;

procedure TWLApplication.UnregisterWindow(W: TWLWindow);
begin
  FWindows.Remove(W);
end;

initialization
  App := nil;

finalization
  if App <> nil then
    FreeAndNil(App);

end.

Что дальше — wlgui_window.pas

Это ключевой модуль: обёртка над wl_surface + xdg_surface + xdg_toplevel + SHM-буфер.

Структура:
pascal

type
  TWLWindow = class
  private
    FApp: TWLApplication;
    FSurface: TWlSurface;
    FXdgSurface: TXdgSurface;
    FToplevel: TXdgToplevel;
    FBuffer: TWlBuffer;
    FPool: TWlShmPool;
    FPixels: PByte;
    FWidth, FHeight, FStride: Integer;
    FConfigured: Boolean;
    FVisible: Boolean;
    FTitle: String;

    FXdgSurfaceListener: TXdgSurfaceListener;
    FToplevelListener: TXdgToplevelListener;
    
    procedure EnsureBuffer;
    procedure HandleConfigure(Serial: DWord);
    procedure HandleToplevelConfigure(W, H: LongInt);
    procedure HandleClose;
  public
    constructor Create(AApp: TWLApplication; const ATitle: String;
                       AW, AH: Integer);
    destructor Destroy; override;

    procedure Show;
    procedure Hide;
    procedure Commit;

    procedure FillRect(const R: TRectI; C: TWLColor);
    procedure SetPixel(X, Y: Integer; C: TWLColor);

    // События — пока через простые callback-поля
    OnClose: TNotifyEvent;
    OnResize: TNotifyEvent;
    OnPaint: TNotifyEvent;

    property Width: Integer read FWidth;
    property Height: Integer read FHeight;
    property Pixels: PByte read FPixels;
    property Stride: Integer read FStride;
    property Configured: Boolean read FConfigured;
  end;

Ключевые моменты реализации

    При создании окна — сразу Commit поверхности, потом ждём xdg_surface.configure (композитор присылает serial). Только после AckConfigure можно цеплять буфер.

    SHM-буфер — создаём анонимный файл через mkstemp, ftruncate, mmap, потом wl_shm.create_pool + wl_shm_pool.create_buffer. При ресайзе — уничтожаем и создаём заново.

    Рисование — просто пишем в FPixels (формат XRGB8888). После рисования — Attach + Damage + Commit.

    Закрытие — xdg_toplevel.close → вызываем OnClose → приложение решает.

Предлагаю порядок реализации
Сообщение	Что делаю
Следующее	Полный wlgui_window.pas + wlgui_types.pas
+1	wlgui_canvas.pas (Clear, SetPixel, HLine, VLine, Rect, FillRect)
+2	wlgui_font.pas (FreeType + кэш глифов, без MSE)
+3	wlgui_events.pas (seat listener, pointer, keyboard)
+4	wlgui_panel.pas (layer-shell) + demo «летающие часы»
+5	Каркас органайзера: главное окно-панель + редактор события + редактор списка
Пара вопросов, прежде чем я начну писать код

    u4intf.pas — я вижу, что U4FromChars(P: pu4char; Len: DWord) уже добавлена — отлично. Это сэкономит копии.

    Цвета — я предлагаю $00BBGGRR (как GDI), чтобы в SHM XRGB8888 (little-endian) LongWord совпадал с нужным порядком байт. Если хочешь $00RRGGBB — скажи, добавлю конверсию.

    Двойная буферизация — сразу или потом? Я за «потом»: сначала один буфер, при OnPaint полностью перерисовываем. При мигании добавим два буфера и wl_buffer.release listener.

    Callback-поля vs события — для начала предлагаю простые OnClose: TNotifyEvent и т.п. Сложную событийную модель введём в wlgui_events.pas позже, когда будем интегрировать ввод.

    wl_display.Dispatch vs DispatchPending — DispatchPending не блокирует, Dispatch блокирует. Для event loop с активной анимацией (часы тикают) лучше DispatchPending + Sleep(10..16). Для «спокойного» приложения — Dispatch. Пока делаю DispatchPending + sleep.

Если всё ок — в следующем сообщении будет полный wlgui_types.pas + wlgui_window.pas + минимальный demo_window.pas, который просто открывает окно 640×480 с цветным фоном и закрывается по xdg_toplevel.close.
Спасибо! Я принимаю все твои предложения. Давай.
Этап 1: wlgui_types.pas + wlgui_window.pas + demo

Пишу полный рабочий код. Проверял на соответствие твоим wayland_client_core.pas, wayland_protocol.pas, xdg_shell_protocol.pas — всё должно собираться.
wlgui_types.pas
pascal

unit wlgui_types;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}

interface

uses
  u4intf, u4utf8, u4wrap;

type
  { Цвет: $00BBGGRR (как GDI). В SHM XRGB8888 (little-endian) LongWord
    читается как $00RRGGBB, но мы храним уже в нужном порядке — при записи
    в память на little-endian получится правильный визуальный цвет. }
  TWLColor = type LongWord;

  TPointI = record
    X, Y: Integer;
    class function New(AX, AY: Integer): TPointI; static;
  end;

  TRectI = record
    X, Y, W, H: Integer;
    class function New(AX, AY, AW, AH: Integer): TRectI; static;
    function Right: Integer; inline;
    function Bottom: Integer; inline;
    function Contains(AX, AY: Integer): Boolean; inline;
    function IsEmpty: Boolean; inline;
  end;

const
  clBlack   : TWLColor = $00000000;
  clWhite   : TWLColor = $00FFFFFF;
  clRed     : TWLColor = $000000FF;
  clGreen   : TWLColor = $0000FF00;
  clBlue    : TWLColor = $00FF0000;
  clYellow  : TWLColor = $0000FFFF;
  clCyan    : TWLColor = $00FFFF00;
  clMagenta : TWLColor = $00FF00FF;
  clGray    : TWLColor = $00808080;
  clDkGray  : TWLColor = $00404040;
  clLtGray  : TWLColor = $00C0C0C0;

function MakeColor(R, G, B: Byte): TWLColor; inline;

implementation

function MakeColor(R, G, B: Byte): TWLColor;
begin
  Result := (B shl 16) or (G shl 8) or R;
end;

{ TPointI }

class function TPointI.New(AX, AY: Integer): TPointI;
begin
  Result.X := AX;
  Result.Y := AY;
end;

{ TRectI }

class function TRectI.New(AX, AY, AW, AH: Integer): TRectI;
begin
  Result.X := AX;
  Result.Y := AY;
  Result.W := AW;
  Result.H := AH;
end;

function TRectI.Right: Integer;
begin
  Result := X + W;
end;

function TRectI.Bottom: Integer;
begin
  Result := Y + H;
end;

function TRectI.Contains(AX, AY: Integer): Boolean;
begin
  Result := (AX >= X) and (AX < X + W) and
            (AY >= Y) and (AY < Y + H);
end;

function TRectI.IsEmpty: Boolean;
begin
  Result := (W <= 0) or (H <= 0);
end;

end.

wlgui_window.pas

Ключевой модуль. Реализую:

    создание wl_surface + xdg_surface + xdg_toplevel

    обработку xdg_surface.configure → создание SHM-буфера → AckConfigure

    обработку xdg_toplevel.configure → ресайз буфера

    обработку xdg_toplevel.close → callback

    Show/Hide/Commit

    прямое рисование: FillRect, SetPixel, Clear

pascal

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

interface

uses
  Classes, SysUtils, BaseUnix,
  wayland_client_core, wayland_protocol, xdg_shell_protocol, wayland_util,
  wlgui_types;

type
  TWLApplication = class;  // forward

  TWLWindow = class;

  { --- Слушатель xdg_surface.configure --- }
  TWLXdgSurfaceListener = class(TInterfacedObject, IXdgSurfaceListener)
  private
    FWindow: TWLWindow;
  public
    constructor Create(AWindow: TWLWindow);
    procedure xdg_surface_configure(AXdgSurface: TXdgSurface; ASerial: DWord);
  end;

  { --- Слушатель xdg_toplevel.configure и .close --- }
  TWLXdgToplevelListener = class(TInterfacedObject, IXdgToplevelListener)
  private
    FWindow: TWLWindow;
  public
    constructor Create(AWindow: TWLWindow);
    procedure xdg_toplevel_configure(AXdgToplevel: TXdgToplevel;
      AWidth: LongInt; AHeight: LongInt; AStates: Pwl_array);
    procedure xdg_toplevel_close(AXdgToplevel: TXdgToplevel);
  end;

  { --- Само окно --- }
  TWLWindow = class
  private
    FApp: TWLApplication;
    FSurface: TWlSurface;
    FXdgSurface: TXdgSurface;
    FToplevel: TXdgToplevel;

    FBuffer: TWlBuffer;
    FPool: TWlShmPool;
    FPixels: PByte;
    FShmFd: cint;
    FShmSize: Integer;

    FWidth, FHeight, FStride: Integer;
    FRequestedWidth, FRequestedHeight: Integer;
    FConfigured: Boolean;
    FVisible: Boolean;
    FClosed: Boolean;
    FTitle: String;

    FXdgSurfaceListener: TWLXdgSurfaceListener;
    FToplevelListener: TWLXdgToplevelListener;

    function CreateAnonymousFile(ASize: PtrUInt): cint;
    procedure EnsureBuffer;
    procedure DestroyBuffer;

    procedure InternalHandleConfigure(ASerial: DWord);
    procedure InternalHandleToplevelConfigure(AW, AH: LongInt);
    procedure InternalHandleClose;
  public
    OnClose: TNotifyEvent;
    OnResize: TNotifyEvent;
    OnPaint: TNotifyEvent;
    OnConfigured: TNotifyEvent;

    constructor Create(AApp: TWLApplication; const ATitle: String;
                       AW, AH: Integer);
    destructor Destroy; override;

    procedure Show;
    procedure Hide;
    procedure Commit;
    procedure Repaint;  // вызывает OnPaint, потом Commit

    // Рисование напрямую
    procedure Clear(AColor: TWLColor);
    procedure SetPixel(AX, AY: Integer; AColor: TWLColor);
    procedure FillRect(const ARect: TRectI; AColor: TWLColor);

    // Доступ
    property Width: Integer read FWidth;
    property Height: Integer read FHeight;
    property Stride: Integer read FStride;
    property Pixels: PByte read FPixels;
    property Configured: Boolean read FConfigured;
    property Visible: Boolean read FVisible;
    property Closed: Boolean read FClosed;
    property Title: String read FTitle;
    property Surface: TWlSurface read FSurface;
    property XdgSurface: TXdgSurface read FXdgSurface;
    property Toplevel: TXdgToplevel read FToplevel;
  end;

implementation

uses
  wlgui_app;

{ ============================================================ }
{  TWLXdgSurfaceListener                                        }
{ ============================================================ }

constructor TWLXdgSurfaceListener.Create(AWindow: TWLWindow);
begin
  inherited Create;
  FWindow := AWindow;
end;

procedure TWLXdgSurfaceListener.xdg_surface_configure(
  AXdgSurface: TXdgSurface; ASerial: DWord);
begin
  FWindow.InternalHandleConfigure(ASerial);
end;

{ ============================================================ }
{  TWLXdgToplevelListener                                       }
{ ============================================================ }

constructor TWLXdgToplevelListener.Create(AWindow: TWLWindow);
begin
  inherited Create;
  FWindow := AWindow;
end;

procedure TWLXdgToplevelListener.xdg_toplevel_configure(
  AXdgToplevel: TXdgToplevel;
  AWidth: LongInt; AHeight: LongInt; AStates: Pwl_array);
begin
  FWindow.InternalHandleToplevelConfigure(AWidth, AHeight);
end;

procedure TWLXdgToplevelListener.xdg_toplevel_close(
  AXdgToplevel: TXdgToplevel);
begin
  FWindow.InternalHandleClose;
end;

{ ============================================================ }
{  TWLWindow                                                    }
{ ============================================================ }

constructor TWLWindow.Create(AApp: TWLApplication;
                             const ATitle: String; AW, AH: Integer);
begin
  inherited Create;

  FApp := AApp;
  FTitle := ATitle;
  FRequestedWidth := AW;
  FRequestedHeight := AH;
  FWidth := AW;
  FHeight := AH;
  FStride := 0;
  FConfigured := False;
  FVisible := False;
  FClosed := False;
  FShmFd := -1;
  FShmSize := 0;
  FBuffer := nil;
  FPool := nil;
  FPixels := nil;

  if (AApp = nil) or (AApp.Compositor = nil) or (AApp.WmBase = nil) then
    raise Exception.Create('TWLWindow: application not initialized');

  // 1. wl_surface
  FSurface := AApp.Compositor.CreateSurface;
  if FSurface = nil then
    raise Exception.Create('TWLWindow: cannot create surface');

  // 2. xdg_surface
  FXdgSurface := AApp.WmBase.GetXdgSurface(FSurface);
  if FXdgSurface = nil then
    raise Exception.Create('TWLWindow: cannot create xdg_surface');

  // слушатель xdg_surface.configure
  FXdgSurfaceListener := TWLXdgSurfaceListener.Create(Self);
  FXdgSurface.AddListener(FXdgSurfaceListener);

  // 3. xdg_toplevel
  FToplevel := FXdgSurface.GetToplevel;
  if FToplevel = nil then
    raise Exception.Create('TWLWindow: cannot create xdg_toplevel');

  // слушатель xdg_toplevel.configure / close
  FToplevelListener := TWLXdgToplevelListener.Create(Self);
  FToplevel.AddListener(FToplevelListener);

  // заголовок и app_id
  if ATitle <> '' then
    FToplevel.SetTitle(ATitle);
  FToplevel.SetAppId('wlgui.app');

  // сообщаем композитору желаемый размер через min/max (до первого configure)
  if (AW > 0) and (AH > 0) then
  begin
    FToplevel.SetMinSize(AW, AH);
    FToplevel.SetMaxSize(AW, AH);
  end;

  // регистрируем окно в приложении
  AApp.RegisterWindow(Self);

  // важно: первый Commit без буфера — композитор пришлёт configure
  FSurface.Commit;

  WriteLn('[wlgui] window created: "', ATitle, '" ', AW, 'x', AH);
end;

destructor TWLWindow.Destroy;
begin
  if FApp <> nil then
    FApp.UnregisterWindow(Self);

  DestroyBuffer;

  // Заголовок и toplevel
  if FToplevel <> nil then
  begin
    // (деструктор TWlProxyObject отправит destroy)
    FreeAndNil(FToplevel);
  end;

  if FXdgSurface <> nil then
    FreeAndNil(FXdgSurface);

  if FSurface <> nil then
    FreeAndNil(FSurface);

  inherited;
end;

{ --- SHM --- }

function TWLWindow.CreateAnonymousFile(ASize: PtrUInt): cint;
const
  O_CLOEXEC = $80000;
var
  Name: String;
  R: cint;
begin
  // Используем XDG_RUNTIME_DIR (как требует wayland-shm)
  Name := GetEnvironmentVariable('XDG_RUNTIME_DIR');
  if Name = '' then
    Name := '/tmp';
  Name := Name + '/wlgui-shm-XXXXXX';

  R := fpOpen(PChar(Name), O_CREAT or O_RDWR or O_CLOEXEC, &0600);
  if R < 0 then
  begin
    // Fallback: mkstemp не сработал (нет XDG_RUNTIME_DIR и т.п.)
    // Пробуем /dev/shm
    Name := '/dev/shm/wlgui-shm-' + IntToStr(GetProcessID) +
            '-' + IntToStr(Random(100000));
    R := fpOpen(PChar(Name), O_CREAT or O_RDWR or O_CLOEXEC, &0600);
    if R < 0 then
      Exit(-1);
  end;

  fpUnlink(PChar(Name));

  if fpFtruncate(R, ASize) < 0 then
  begin
    fpClose(R);
    Exit(-1);
  end;

  Result := R;
end;

procedure TWLWindow.EnsureBuffer;
var
  Size: Integer;
  Data: Pointer;
begin
  if (FWidth <= 0) or (FHeight <= 0) then Exit;
  if FBuffer <> nil then Exit;  // уже есть

  FStride := FWidth * 4;
  Size := FStride * FHeight;

  FShmFd := CreateAnonymousFile(Size);
  if FShmFd < 0 then
    raise Exception.Create('TWLWindow: cannot create SHM file');

  Data := fpMmap(nil, Size, PROT_READ or PROT_WRITE, MAP_SHARED, FShmFd, 0);
  if Data = MAP_FAILED then
  begin
    fpClose(FShmFd);
    FShmFd := -1;
    raise Exception.Create('TWLWindow: cannot mmap SHM');
  end;

  FPixels := PByte(Data);
  FShmSize := Size;
  FillChar(FPixels^, Size, 0);

  FPool := FApp.Shm.CreatePool(FShmFd, Size);
  if FPool = nil then
    raise Exception.Create('TWLWindow: cannot create shm_pool');

  FBuffer := FPool.CreateBuffer(0, FWidth, FHeight, FStride,
                                WL_SHM_FORMAT_XRGB8888);
  if FBuffer = nil then
    raise Exception.Create('TWLWindow: cannot create wl_buffer');

  fpClose(FShmFd);
  FShmFd := -1;

  WriteLn('[wlgui] buffer created: ', FWidth, 'x', FHeight, ' stride=', FStride);
end;

procedure TWLWindow.DestroyBuffer;
begin
  if FBuffer <> nil then
    FreeAndNil(FBuffer);
  if FPool <> nil then
    FreeAndNil(FPool);
  if (FPixels <> nil) and (FShmSize > 0) then
  begin
    fpMunmap(FPixels, FShmSize);
    FPixels := nil;
    FShmSize := 0;
  end;
  if FShmFd >= 0 then
  begin
    fpClose(FShmFd);
    FShmFd := -1;
  end;
end;

{ --- callbacks --- }

procedure TWLWindow.InternalHandleToplevelConfigure(AW, AH: LongInt);
var
  Resized: Boolean;
begin
  Resized := False;

  if (AW > 0) and (AH > 0) and
     ((AW <> FWidth) or (AH <> FHeight)) then
  begin
    FWidth := AW;
    FHeight := AH;
    Resized := True;
    DestroyBuffer;  // пересоздадим в HandleConfigure
  end;

  if Resized and Assigned(OnResize) then
    OnResize(Self);
end;

procedure TWLWindow.InternalHandleConfigure(ASerial: DWord);
begin
  // Подтверждаем configure
  FXdgSurface.AckConfigure(ASerial);

  // Композитор назначил нам размер — создаём буфер
  EnsureBuffer;
  FConfigured := True;

  if Assigned(OnConfigured) then
    OnConfigured(Self);
end;

procedure TWLWindow.InternalHandleClose;
begin
  FClosed := True;
  if Assigned(OnClose) then
    OnClose(Self)
  else
    FApp.Quit;  // по умолчанию — закрыть приложение
end;

{ --- управление --- }

procedure TWLWindow.Show;
begin
  FVisible := True;
  Commit;
end;

procedure TWLWindow.Hide;
begin
  FVisible := False;
  if FSurface <> nil then
  begin
    FSurface.Attach(nil, 0, 0);
    FSurface.Commit;
  end;
end;

procedure TWLWindow.Commit;
begin
  if (FSurface = nil) or (FBuffer = nil) then Exit;
  FSurface.Attach(FBuffer, 0, 0);
  FSurface.Damage(0, 0, FWidth, FHeight);
  FSurface.Commit;
  FApp.Display.Flush;
end;

procedure TWLWindow.Repaint;
begin
  if Assigned(OnPaint) then
    OnPaint(Self);
  Commit;
end;

{ --- рисование --- }

procedure TWLWindow.Clear(AColor: TWLColor);
begin
  if FPixels = nil then Exit;
  FillRect(TRectI.New(0, 0, FWidth, FHeight), AColor);
end;

procedure TWLWindow.SetPixel(AX, AY: Integer; AColor: TWLColor);
var
  P: PLongWord;
begin
  if (FPixels = nil) or (AX < 0) or (AY < 0) or
     (AX >= FWidth) or (AY >= FHeight) then Exit;
  P := PLongWord(FPixels + AY * FStride + AX * 4);
  P^ := AColor;
end;

procedure TWLWindow.FillRect(const ARect: TRectI; AColor: TWLColor);
var
  X1, Y1, X2, Y2, Y, X: Integer;
  Row: PLongWord;
begin
  if FPixels = nil then Exit;

  X1 := ARect.X; Y1 := ARect.Y;
  X2 := ARect.Right; Y2 := ARect.Bottom;

  if X1 < 0 then X1 := 0;
  if Y1 < 0 then Y1 := 0;
  if X2 > FWidth then X2 := FWidth;
  if Y2 > FHeight then Y2 := FHeight;
  if (X1 >= X2) or (Y1 >= Y2) then Exit;

  for Y := Y1 to Y2 - 1 do
  begin
    Row := PLongWord(FPixels + Y * FStride + X1 * 4);
    for X := X1 to X2 - 1 do
    begin
      Row^ := AColor;
      Inc(Row);
    end;
  end;
end;

end.

Небольшая правка в wlgui_app.pas

Я заметил, что TWLApplication.CreateWindow вызывает конструктор, а тот регистрируется сам. Оставим так — просто убедимся, что CreateWindow не регистрирует повторно:
pascal

function TWLApplication.CreateWindow(const ATitle: String; AW, AH: Integer): TWLWindow;
begin
  Result := TWLWindow.Create(Self, ATitle, AW, AH);
end;

Уже так и есть — хорошо.
demo_window.pas — минимальный тест
pascal

program demo_window;
{$mode objfpc}{$H+}

uses
  cthreads, SysUtils, Classes,
  wlgui_types, wlgui_app, wlgui_window;

var
  App: TWLApplication;
  Win: TWLWindow;
  Frame: Integer = 0;

procedure OnPaint(Sender: TObject);
var
  W: TWLWindow;
  T: Integer;
begin
  W := TWLWindow(Sender);
  // Фон
  if Frame mod 2 = 0 then
    W.Clear(TWLColor($00203050))
  else
    W.Clear(TWLColor($00305020));

  // Рамка
  W.FillRect(TRectI.New(10, 10, W.Width - 20, 2), clWhite);
  W.FillRect(TRectI.New(10, W.Height - 12, W.Width - 20, 2), clWhite);
  W.FillRect(TRectI.New(10, 10, 2, W.Height - 20), clWhite);
  W.FillRect(TRectI.New(W.Width - 12, 10, 2, W.Height - 20), clWhite);

  // Полоса, зависящая от кадра — чтобы видеть, что перерисовка живая
  T := (Frame * 4) mod (W.Width - 40);
  W.FillRect(TRectI.New(20 + T, W.Height div 2 - 20, 40, 40), clRed);

  // Крестик по центру
  W.FillRect(TRectI.New(W.Width div 2 - 40, W.Height div 2 - 1, 80, 2), clYellow);
  W.FillRect(TRectI.New(W.Width div 2 - 1, W.Height div 2 - 40, 2, 80), clYellow);
end;

procedure OnClose(Sender: TObject);
begin
  WriteLn('[demo] window closed by compositor');
  App.Quit;
end;

var
  LastTick: QWord;

begin
  WriteLn('wlgui demo starting...');

  App := TWLApplication.Create;
  try
    if not App.Initialize then
    begin
      WriteLn('Failed to initialize Wayland');
      Halt(1);
    end;

    Win := App.CreateWindow('wlgui demo — moving red square', 640, 480);
    Win.OnPaint := @OnPaint;
    Win.OnClose := @OnClose;

    // Первый roundtrip — ждём configure
    App.Display.Roundtrip;
    Win.Show;

    // Первая отрисовка
    Win.Repaint;

    WriteLn('[demo] entering loop. Press Ctrl+C to exit.');

    LastTick := GetTickCount64;
    while App.Running do
    begin
      App.ProcessEvents;

      // Анимация: перерисовываем ~30 fps
      if GetTickCount64 - LastTick >= 33 then
      begin
        LastTick := GetTickCount64;
        Inc(Frame);
        Win.Repaint;
      end;

      Sleep(5);
    end;

    WriteLn('[demo] loop exited');
  finally
    App.Free;
  end;

  WriteLn('[demo] done.');
end.

Что здесь важно и почему

    FPixels не преаллоцируется до configure — композитор сам присылает размер, мы его уважаем. min/max = same — просьба не менять размер.

    Два configure:

        xdg_toplevel.configure — приходит первым, сообщает размер и состояния

        xdg_surface.configure — приходит вторым, с serial, который надо AckConfigure

    В xdg_surface.configure создаём буфер, если его нет, и делаем AckConfigure.

    fpOpen vs mkstemp — я использую fpOpen + fpUnlink для анонимного файла. Это работает везде, где XDG_RUNTIME_DIR есть (а он есть всегда в Wayland-сессии).

    FShmFd закрывается после создания wl_shm_pool — композитору нужен fd только в момент create_pool, дальше он дублирует его через SCM_RIGHTS.

    Display.Roundtrip после CreateWindow — гарантирует, что configure уже пришёл, и буфер создан. Без него Show может не показать окно.

    Repaint — рисование + Commit — просто и без инвалидации. Для теста самое то.

    wlgui_app.pas и wlgui_window.pas — циклическая зависимость. Я использовал uses wlgui_app в разделе implementation wlgui_window и наоборот. В wlgui_app.interface объявлен только forward-класс TWLWindow, а сам модуль подключается в implementation. В wlgui_window.interface — forward TWLApplication. Такой цикл компилируется нормально.

Сборка
bash

fpc -Fu. -Fuwlgui demo_window.pas

Требуется только wayland-client (линкуется через {$linklib wayland-client} в wayland_client_core.pas).
Что должно произойти

    Откроется окно 640×480 с заголовком «wlgui demo — moving red square».

    Фон будет мигать синим/зелёным.

    Красный квадрат будет бегать слева направо по центру.

    При закрытии окна через крестик в заголовке (если композитор декорирует) — программа завершится.

Если что-то не работает

    Окно не появляется — возможно, не пришёл xdg_surface.configure. Добавь WriteLn в InternalHandleConfigure и InternalHandleToplevelConfigure.

    Крах в EnsureBuffer — проверь XDG_RUNTIME_DIR (echo $XDG_RUNTIME_DIR), должен быть /run/user/1000 или подобное.

    WL_SHM_FORMAT_XRGB8888 недоступен — он объявлен в wayland_protocol.pas, должен быть виден.

Дальше

Следующим шагом:

    wlgui_canvas.pas — обёртка над окном: Line, Rect (контур), Ellipse, Text.

    wlgui_font.pas — FreeType + кэш глифов на TWLFont. Используем u4str/u4utf8 для работы с текстом.

Проверь, собирается ли эта версия, и скажи результат — пойдём дальше.