git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit8a50242f17c1c3e83b8f965e0f27087790f2f4f6
parent414cd86471
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 08:13
feat(app)!: Application::create is required; the legacy new is gone

Application::new(qh, sender) took the session's Wayland queue handle —
which no client ever used — and calloop's sender, and defaulted to
forwarding into create; create defaulted to a panic naming the app. Every
client implements create now (27 crates moved in the same sweep), so new
is removed, the runner calls A::create(AppSender::from(..)) as the
AppKit and browser shells already did, and create has no default: an app
without a constructor is a compile error, not a crash at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                    | 13 +++++++------
 src/backend/app.rs           | 33 ++++++++-------------------------
 src/backend/window_runner.rs |  2 +-
 3 files changed, 16 insertions(+), 32 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 776bf1c..f1debe0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -520,12 +520,13 @@ Key methods (see the trait def in `backend/app.rs`):
   `AppSender` is cce-ui's own handle — `send`, `Clone`, `Send`, and `From` both ways
   with `calloop::channel::Sender` for a client that still stores calloop's type — so
   the constructor names no window system, which is what lets a second shell (macOS,
-  the browser) run the same `Application`. The legacy `new(qh, sender)` still works:
-  the runner calls `new`, whose default forwards to `create`, so a client implements
-  ONE of the two and moves when it likes (no client ever used `qh`). Implementing
-  neither panics at startup naming the app. `new` is removed once no client
-  implements it, the way `VkRenderer::new` went. `register_sources` stays a
-  calloop-only hook: it is the Wayland shell's, not part of the portable contract.
+  the browser) run the same `Application`. `create` is REQUIRED (since 2026-10-08):
+  the legacy `new(qh, sender)`, whose queue handle no client ever used, is gone, and
+  so is the default that panicked at startup when an app implemented neither, so a
+  missing constructor is a compile error. An app that keeps calloop's sender converts
+  on the first line (`let tx: calloop::channel::Sender<_> = sender.into();`).
+  `register_sources` stays a calloop-only hook: it is the Wayland shell's, not part of
+  the portable contract.
 - **Draw**: `view` / `view_rounded_quads` / `view_vectors` / `overlay_quads` push legacy
   primitive tuples; `text_items()` returns text; `custom_vertices()` appends raw vertices (e.g.
   graph geometry). `display_list()` is the new opt-in path (see below).
diff --git a/src/backend/app.rs b/src/backend/app.rs
index 6caa3d8..ffb8e24 100644
--- a/src/backend/app.rs
+++ b/src/backend/app.rs
@@ -5,8 +5,6 @@
 
 use cosmic_text::FontSystem;
 use crate::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent};
-#[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
-use wayland_client::QueueHandle;
 #[cfg(not(target_arch = "wasm32"))]
 use crate::vk::VkRenderer;
 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
@@ -206,30 +204,15 @@ impl<M> From<AppSender<M>> for calloop::channel::Sender<M> {
 pub trait Application: Sized + 'static {
     type Message: Send + Clone + 'static;
 
-    /// Build the app. Implement this or the legacy [`new`](Self::new), not
-    /// both: the runner calls `new`, whose default forwards here. `create`
-    /// takes no Wayland type, so it is the constructor another shell can
-    /// call; `new` goes once no client implements it.
+    /// Build the app. `sender` posts messages to [`update`](Self::update) from any thread, and
+    /// waking the loop if it sleeps; it converts into calloop's `Sender` for an app that keeps
+    /// that type (`let tx: calloop::channel::Sender<_> = sender.into();`). It names no window
+    /// system, so every shell (Wayland, AppKit, the browser) builds the app the same way.
     ///
-    /// Implementing neither panics at startup, naming the app: the price of
-    /// letting clients move one at a time.
-    fn create(sender: AppSender<Self::Message>) -> Self {
-        let _ = sender;
-        panic!(
-            "{}: implement Application::create (or the legacy Application::new)",
-            std::any::type_name::<Self>()
-        )
-    }
-
-    /// The legacy constructor, from before the runner had a second shell in
-    /// view: the session's Wayland queue handle (no client ever used it) and
-    /// its calloop sender. Prefer [`create`](Self::create); the default here
-    /// forwards to it. Native only: it names the Wayland queue.
-    #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
-    fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
-        let _ = qh;
-        Self::create(AppSender::from(sender))
-    }
+    /// Required since 2026-10-07: the legacy `new(qh, sender)` — whose Wayland queue handle no
+    /// client ever used — and the default that panicked when neither was implemented are gone,
+    /// so an app without a constructor is a compile error rather than a crash at startup.
+    fn create(sender: AppSender<Self::Message>) -> Self;
     fn settings(&self) -> WindowSettings;
     /// Return `Some(..)` to run on a wlr-layer-shell surface (overlay/panel)
     /// instead of an xdg toplevel. Defaults to `None` (a normal window).
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 9479cc5..b6df778 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2105,7 +2105,7 @@ fn run_session<'l, A: Application>(
     // worker threads that are still holding the original.
     let inner = match existing_app {
         Some(app) => app,
-        None => A::new(&qh, engine_state.sender.clone()),
+        None => A::create(AppSender::from(engine_state.sender.clone())),
     };
     let settings = inner.settings();
     crate::scale::set_app_id(settings.app_id.clone());