GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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());