GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
build: the library builds for wasm32-unknown-unknown
The first step of the browser port (W1): everything that is not the
native shell or renderer now compiles for the browser target, checked
by scripts/check-wasm (cargo check --lib --target
wasm32-unknown-unknown, with and without markdown/doc_editor).
- Cargo: ash, gpu-allocator, naga, smithay-client-toolkit, calloop,
calloop-wayland-source, wayland-*, raw-window-handle, xkeysym, libc
and rfd move under cfg(not(target_arch = "wasm32")). Everything left
(cosmic-text, resvg, png, kdl, serde, glam, cursor-icon, ...) builds
for wasm as it is. web-time joins for all targets.
- Modules: vk, wayland, protocol, ipc, mcp, file_dialog and the Wayland
shell (backend::{window_runner, menu_popup, dnd}) are native only.
engine.rs and backend/mod.rs re-export from the portable modules
(app, driver, tessellate, text) with the native items (run,
EngineState, xdg_toplevel, the layer-shell types) behind the cfg;
every name a client imported is still there natively.
- The client contract: Application::new(qh, ..), layer() and
LayerSettings, register_sources, renderer_init and stage_renderer
(both take a VkRenderer) are native only. WindowAction::Resize takes
app::WindowEdge, which is xdg_toplevel's ResizeEdge on Linux exactly
as before and the driver's ResizeEdge in the browser. AppSender wraps
calloop's sender natively and a std::sync::mpsc sender in the
browser, still Send either way.
- Time: portable code takes Instant from web-time (15 files). It IS
std's type natively; std's panics in the browser, which has no clock
of its own. config.rs keeps std's SystemTime: it only holds file
modification times from metadata, never now().
- The ColorSelector's request that the compositor place its picker at
the pointer moves into place_picker_at_pointer, native only.
Native is unchanged: test results the same (523 pass, the 2
pre-existing failures), the plate golden byte-identical, the demo
identical to the pixel over the 24 scripted steps, and the loop's
cadence the same (5 wake-ups in 5 s idle, 125-126 in 2 s of a held key
for the old binary and the new alike).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjL3pejMNY95NHv9BcmXaZ
CLAUDE.md | 13 ++++++++
Cargo.lock | 11 +++++++
Cargo.toml | 39 ++++++++++++++----------
scripts/check-wasm | 16 ++++++++++
src/backend/app.rs | 44 +++++++++++++++++++++++----
src/backend/driver.rs | 2 +-
src/backend/mod.rs | 18 +++++++----
src/backend/shell.rs | 3 +-
src/backend/text.rs | 8 ++---
src/context.rs | 4 +--
src/engine.rs | 23 ++++++++++----
src/lib.rs | 10 +++++++
src/motion.rs | 3 +-
src/widget/container/treelist.rs | 4 +--
src/widget/core.rs | 10 +++----
src/widget/display/graph.rs | 4 +--
src/widget/doc_editor/mod.rs | 3 +-
src/widget/input/color_selector.rs | 61 +++++++++++++++++++++-----------------
src/widget/input/dropdown.rs | 8 ++---
src/widget/input/ramp.rs | 4 +--
src/widget/input/slider.rs | 4 +--
src/widget/line_edit.rs | 3 +-
src/widget/scroll_motion.rs | 2 +-
src/widget/side_swipe.rs | 3 +-
24 files changed, 209 insertions(+), 91 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 70231ad..35fbd44 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -57,6 +57,19 @@ scattering of widgets (`text_box`, `slider`, `dropdown`, `treelist`, …). When
engine, that module's tests are the fast feedback loop; run `cargo test -p cce-ui scene::` before
anything else.
+**The library also builds for the browser** (`wasm32-unknown-unknown`, since 2026-10-04):
+`scripts/check-wasm` type-checks it, with and without the optional features. The native
+shell and renderer — `vk`, the Wayland shell (`backend::{window_runner, menu_popup, dnd}`),
+`wayland`, `protocol`, `ipc`, `mcp`, `file_dialog` — and their crates (ash, smithay, calloop,
+wayland-*, libc, rfd) are `cfg(not(target_arch = "wasm32"))`; so are the Wayland-typed parts
+of the client contract: `Application::new(qh, …)`, `layer()` and `LayerSettings`,
+`register_sources`, and `renderer_init` / `stage_renderer`, which take a `VkRenderer`.
+`WindowAction::Resize` takes `app::WindowEdge` — xdg's `ResizeEdge` on Linux, as before.
+Portable code keeps time with `web_time::Instant` (std's own type natively; std's panics in
+the browser), and reaches what a renderer draws through `crate::draw`, not `crate::vk`. A
+change that makes portable code call into a native module fails `check-wasm` first — put
+the native half behind the cfg, as `color_selector::place_picker_at_pointer` does.
+
Wayland protocol bindings are generated **inline at compile time** by `wayland-scanner` macros in
`src/protocol.rs` from `protocol/*.xml` (`cce-inspector-v1`, `cce-window-management-v1`) — there is
no `build.rs` and no codegen step to run.
diff --git a/Cargo.lock b/Cargo.lock
index 27631e3..aecb135 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -377,6 +377,7 @@ dependencies = [
"wayland-client",
"wayland-protocols 0.31.2",
"wayland-scanner",
+ "web-time",
"xkeysym",
]
@@ -2570,6 +2571,16 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "weezl"
version = "0.1.12"
diff --git a/Cargo.toml b/Cargo.toml
index 99eff0b..42e03f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,17 +18,38 @@ doc_editor = []
[dependencies]
cce-vault = { git = "https://github.com/lsgalante/cce-vault.git", rev = "41ca52bd51ea3655a791c58005be9fce939e4333", optional = true }
-raw-window-handle = "0.6"
glam = "0.29"
bytemuck = { version = "1", features = ["derive"] }
# Text shaping. This was reached through glyphon until the wgpu path was retired;
# every `glyphon::` item cce-ui ever used was a cosmic-text re-export (the sole
-# exception, `TextBounds`, is now defined in backend/window_runner.rs), so depending
+# exception, `TextBounds`, is now defined in backend/text.rs), so depending
# on cosmic-text directly drops glyphon, wgpu and 17 more crates from the build —
# the rest of wgpu's tree stays only because naga/ash below already pull it.
# Pinned to the exact version glyphon 0.8 resolved to, so shaping does not change:
# verified by diffing shaped glyph ids/advances both ways over 288 runs.
cosmic-text = "=0.12.1"
+bitflags = "2"
+# The cursor names an app returns. smithay-client-toolkit re-exports this same
+# crate's type, so the Wayland shell hands it over unconverted; depending on it
+# directly keeps the client contract free of the toolkit.
+cursor-icon = "1.2"
+resvg = "0.41.0"
+# App/tray icon themes are overwhelmingly PNG; resvg covers only the SVG half.
+png = "0.17"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+kdl = "4.6"
+log = "0.4"
+# `Instant` that works in the browser: std's panics on wasm32-unknown-unknown,
+# which has no clock of its own. On every other target this IS std's.
+web-time = "1"
+
+# The native shell and renderer: Wayland through smithay-client-toolkit on a
+# calloop loop, Vulkan through ash. None of it builds for the browser, where
+# the shell and renderer are the web platform's (`cargo check --lib --target
+# wasm32-unknown-unknown` is the check that keeps the rest portable).
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+raw-window-handle = "0.6"
# Raw-Vulkan backend (versions match what wgpu 24 already pulled in).
ash = "0.38"
gpu-allocator = { version = "0.27", default-features = false, features = ["vulkan"] }
@@ -40,20 +61,6 @@ wayland-client = { version = "0.31", features = ["system"] }
wayland-protocols = { version = "0.31", features = ["client", "unstable"] }
wayland-backend = "0.3"
wayland-scanner = "0.31"
-bitflags = "2"
-# The cursor names an app returns. smithay-client-toolkit re-exports this same
-# crate's type, so the Wayland shell hands it over unconverted; depending on it
-# directly keeps the client contract free of the toolkit.
-cursor-icon = "1.2"
xkeysym = "0.2"
-resvg = "0.41.0"
-# App/tray icon themes are overwhelmingly PNG; resvg covers only the SVG half.
-png = "0.17"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-kdl = "4.6"
libc = "0.2"
-log = "0.4"
rfd = "0.15"
-
-
diff --git a/scripts/check-wasm b/scripts/check-wasm
new file mode 100755
index 0000000..581ac62
--- /dev/null
+++ b/scripts/check-wasm
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# check-wasm — does the portable part of cce-ui still build for the browser?
+# Type-checks the library for wasm32-unknown-unknown, with and without the
+# optional features. The native shell and renderer (Wayland, Vulkan, the
+# compositor IPC) are cfg'd out there; everything else must compile, and a
+# change that reaches for one of them from portable code fails here first.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+if ! rustup target list --installed 2>/dev/null | grep -qx wasm32-unknown-unknown; then
+ echo "check-wasm: the wasm32-unknown-unknown target is not installed:" >&2
+ echo " rustup target add wasm32-unknown-unknown" >&2
+ exit 2
+fi
+cargo check --lib --target wasm32-unknown-unknown
+cargo check --lib --target wasm32-unknown-unknown --features markdown,doc_editor
+echo "check-wasm: ok"
diff --git a/src/backend/app.rs b/src/backend/app.rs
index 2257387..60e4b3f 100644
--- a/src/backend/app.rs
+++ b/src/backend/app.rs
@@ -3,11 +3,13 @@
//! Moved out of `window_runner` unchanged; still re-exported from there
//! (and from `engine`) at the old paths.
-use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel;
-use wayland_client::QueueHandle;
use cosmic_text::FontSystem;
use crate::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent};
+#[cfg(not(target_arch = "wasm32"))]
+use wayland_client::QueueHandle;
+#[cfg(not(target_arch = "wasm32"))]
use crate::vk::VkRenderer;
+#[cfg(not(target_arch = "wasm32"))]
use super::window_runner::EngineState;
use cursor_icon::CursorIcon;
use super::tessellate::Vertex;
@@ -22,16 +24,25 @@ pub struct WindowSettings {
pub min_size: Option<(u32, u32)>,
}
+/// The edge a [`WindowAction::Resize`] grabs. On Wayland it is
+/// `xdg_toplevel`'s own enum, as it always was; elsewhere the driver's.
+#[cfg(not(target_arch = "wasm32"))]
+pub use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as WindowEdge;
+#[cfg(target_arch = "wasm32")]
+pub use super::driver::ResizeEdge as WindowEdge;
+
/// A compositor-side window operation requested by the app: an interactive
/// move or resize grab. Returned from [`Application::take_window_action`];
/// the runner executes it with the serial of the most recent pointer press.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowAction {
Move,
- Resize(xdg_toplevel::ResizeEdge),
+ Resize(WindowEdge),
}
// Re-export the wlr-layer-shell types apps need to describe a layer surface.
+// Layer surfaces are a Wayland (wlr) notion: native only, like `layer()`.
+#[cfg(not(target_arch = "wasm32"))]
pub use smithay_client_toolkit::shell::wlr_layer::{
Anchor as LayerAnchor, KeyboardInteractivity as LayerKeyboardInteractivity, Layer as LayerKind,
};
@@ -39,6 +50,7 @@ pub use smithay_client_toolkit::shell::wlr_layer::{
/// Opt-in configuration for running an [`Application`] on a wlr-layer-shell
/// surface (panels, overlays, notifications) instead of an xdg toplevel.
/// Return one from [`Application::layer`] to select layer-shell.
+#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone)]
pub struct LayerSettings {
pub layer: LayerKind,
@@ -87,9 +99,15 @@ pub struct RenderContext<'a> {
/// It names no window system. Handed to [`Application::create`], it is what
/// a client written against it can be run by any shell with; the Wayland
/// runner backs it with its calloop channel, and `From` converts both ways
-/// for a client that still keeps a `calloop::channel::Sender` somewhere.
+/// for a client that still keeps a `calloop::channel::Sender` somewhere. In
+/// the browser it is a `std::sync::mpsc` sender whose receiver the shell
+/// drains every turn — still `Send`, so app code that hands it to a worker
+/// compiles unchanged.
pub struct AppSender<M> {
+ #[cfg(not(target_arch = "wasm32"))]
inner: calloop::channel::Sender<M>,
+ #[cfg(target_arch = "wasm32")]
+ inner: std::sync::mpsc::Sender<M>,
}
impl<M> AppSender<M> {
@@ -112,12 +130,21 @@ impl<M> std::fmt::Debug for AppSender<M> {
}
}
+#[cfg(not(target_arch = "wasm32"))]
impl<M> From<calloop::channel::Sender<M>> for AppSender<M> {
fn from(inner: calloop::channel::Sender<M>) -> Self {
Self { inner }
}
}
+#[cfg(target_arch = "wasm32")]
+impl<M> From<std::sync::mpsc::Sender<M>> for AppSender<M> {
+ fn from(inner: std::sync::mpsc::Sender<M>) -> Self {
+ Self { inner }
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
impl<M> From<AppSender<M>> for calloop::channel::Sender<M> {
fn from(sender: AppSender<M>) -> Self {
sender.inner
@@ -145,7 +172,8 @@ pub trait Application: Sized + 'static {
/// 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.
+ /// forwards to it. Native only: it names the Wayland queue.
+ #[cfg(not(target_arch = "wasm32"))]
fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
let _ = qh;
Self::create(AppSender::from(sender))
@@ -153,6 +181,7 @@ pub trait Application: Sized + 'static {
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).
+ #[cfg(not(target_arch = "wasm32"))]
fn layer(&self) -> Option<LayerSettings> {
None
}
@@ -282,6 +311,7 @@ pub trait Application: Sized + 'static {
[0.0, 0.0, 0.0, 0.0]
}
+ #[cfg(not(target_arch = "wasm32"))]
fn register_sources(&mut self, _handle: &calloop::LoopHandle<'_, EngineState<Self>>) {}
fn adjust_size(&self, width: f32, height: f32) -> (f32, f32) {
@@ -418,6 +448,7 @@ pub trait Application: Sized + 'static {
/// Called once, right after the renderer is created and before the first
/// frame: create persistent renderer resources here (3D meshes via
/// [`VkRenderer::create_mesh`]). Most 2D apps never need this.
+ #[cfg(not(target_arch = "wasm32"))]
fn renderer_init(&mut self, _renderer: &mut VkRenderer) {}
/// Direct renderer staging, called every frame after the engine's own text
@@ -428,6 +459,7 @@ pub trait Application: Sized + 'static {
/// the renderer's text state, the engine never touches it). Return `true`
/// to request another frame immediately (e.g. while a path tracer is still
/// accumulating samples).
+ #[cfg(not(target_arch = "wasm32"))]
fn stage_renderer(&mut self, _renderer: &mut VkRenderer, _size: LogicalSize, _scale: f64) -> bool {
false
}
@@ -495,7 +527,7 @@ pub trait Application: Sized + 'static {
}
-#[cfg(test)]
+#[cfg(all(test, not(target_arch = "wasm32")))]
mod app_sender_tests {
use super::AppSender;
use calloop::channel::{channel, Event};
diff --git a/src/backend/driver.rs b/src/backend/driver.rs
index cf9074b..de5f978 100644
--- a/src/backend/driver.rs
+++ b/src/backend/driver.rs
@@ -13,7 +13,7 @@
//! them (the Wayland runner keeps them on `EngineState`, where clients'
//! `register_sources` callbacks reach `inner` and `redraw` directly).
-use std::time::Instant;
+use web_time::Instant;
use super::app::{Application, LogicalPosition, LogicalSize};
use cursor_icon::CursorIcon;
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 71359c7..d4d4b4a 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -2,13 +2,19 @@ pub mod app;
pub mod driver;
pub mod frame;
pub mod shell;
-pub mod dnd;
-pub mod menu_popup;
pub mod tessellate;
pub mod text;
+// The Wayland shell: native only.
+#[cfg(not(target_arch = "wasm32"))]
+pub mod dnd;
+#[cfg(not(target_arch = "wasm32"))]
+pub mod menu_popup;
+#[cfg(not(target_arch = "wasm32"))]
pub mod window_runner;
-pub use window_runner::{
- EngineState, WindowSettings, LogicalPosition, LogicalSize, Application, AppSender, run,
- Vertex, LineCap, PressedKey, get_text_buffer,
-};
+pub use app::{Application, AppSender, LogicalPosition, LogicalSize, WindowSettings};
+pub use driver::PressedKey;
+pub use tessellate::{LineCap, Vertex};
+pub use text::get_text_buffer;
+#[cfg(not(target_arch = "wasm32"))]
+pub use window_runner::{run, EngineState};
diff --git a/src/backend/shell.rs b/src/backend/shell.rs
index be2c40f..78dc71e 100644
--- a/src/backend/shell.rs
+++ b/src/backend/shell.rs
@@ -11,7 +11,8 @@
//! loop — the browser's animation frames, an AppKit run loop — calls `turn`
//! from wherever its loop turns and sleeps (or schedules) for what it says.
-use std::time::{Duration, Instant};
+use std::time::Duration;
+use web_time::Instant;
use super::app::Application;
use super::driver::{Driver, Turn};
diff --git a/src/backend/text.rs b/src/backend/text.rs
index e57aad0..be03db4 100644
--- a/src/backend/text.rs
+++ b/src/backend/text.rs
@@ -19,7 +19,7 @@ struct BufferCacheKey {
#[derive(Clone)]
struct CachedBuffer {
buffer: Buffer,
- last_accessed: std::time::Instant,
+ last_accessed: web_time::Instant,
}
std::thread_local! {
@@ -124,7 +124,7 @@ pub fn get_text_buffer_attrs(
let cached = BUFFER_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(cached_item) = cache.get_mut(&key) {
- cached_item.last_accessed = std::time::Instant::now();
+ cached_item.last_accessed = web_time::Instant::now();
Some(cached_item.buffer.clone())
} else {
None
@@ -221,7 +221,7 @@ pub fn get_text_buffer_attrs(
BUFFER_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if cache.len() >= 2000 {
- let mut items: Vec<(BufferCacheKey, std::time::Instant)> = cache
+ let mut items: Vec<(BufferCacheKey, web_time::Instant)> = cache
.iter()
.map(|(k, v)| (k.clone(), v.last_accessed))
.collect();
@@ -232,7 +232,7 @@ pub fn get_text_buffer_attrs(
}
cache.insert(key, CachedBuffer {
buffer: buf.clone(),
- last_accessed: std::time::Instant::now(),
+ last_accessed: web_time::Instant::now(),
});
});
diff --git a/src/context.rs b/src/context.rs
index 3cdd45c..5921fb7 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -86,7 +86,7 @@ pub struct UiContext {
/// inertia), so a second tick per frame would run them at double speed.
tick_count: u64,
pub spatial_grid: SpatialGrid,
- pub last_scroll_time: Option<std::time::Instant>,
+ pub last_scroll_time: Option<web_time::Instant>,
pub scroll_initiate_widget_id: Option<WidgetId>,
pub scroll_gesture_new: bool,
pub ctrl_pressed: bool,
@@ -148,7 +148,7 @@ impl UiContext {
/// take it (2026-09-20: the Settings list would not scroll after the
/// params pane had).
pub fn note_scroll_event(&mut self) {
- let now = std::time::Instant::now();
+ let now = web_time::Instant::now();
let elapsed_ms = match self.last_scroll_time {
None => 999999,
Some(last) => now.duration_since(last).as_millis(),
diff --git a/src/engine.rs b/src/engine.rs
index 68a3a8d..f0571ae 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -1,8 +1,13 @@
-pub use crate::backend::window_runner::{
- Vertex, LineCap, WindowSettings, LogicalPosition, LogicalSize,
- RenderContext, Application, AppSender, PressedKey, EngineState, run,
- WindowAction, xdg_toplevel, PointerCursorIcon as CursorIcon,
- LayerSettings, LayerKind, LayerAnchor, LayerKeyboardInteractivity,
+//! The client-facing names, re-exported in one place: what an app's
+//! `use cce_ui::engine::{...}` reaches. The portable contract first; the
+//! native shell's entry point and Wayland-typed items after it.
+
+pub use crate::backend::app::{
+ Application, AppSender, LogicalPosition, LogicalSize, RenderContext, WindowAction, WindowSettings,
+};
+pub use crate::backend::driver::PressedKey;
+pub use crate::backend::tessellate::{
+ Vertex, LineCap,
quad_vertices, quad_vertices_with_clip, quad_vertices_clipped, line_vertices,
vector_vertices, rounded_rect_vertices_corners, push_rounded_rect_vertices_corners,
rounded_rect_vertices, push_rounded_rect_vertices, plate_bevel_vertices,
@@ -10,5 +15,11 @@ pub use crate::backend::window_runner::{
extra_quad_vertices, push_extra_quad_vertices, extra_quad_vertices_clipped,
push_extra_quad_vertices_clipped, circle_vertices, circle_border_vertices,
arc_background_vertices, push_arc_background_vertices, push_plate_solid_border_vertices,
- get_text_buffer_laid_out, shaped_cluster_offsets, shaping_for,
};
+pub use crate::backend::text::{get_text_buffer_laid_out, shaped_cluster_offsets, shaping_for};
+pub use cursor_icon::CursorIcon;
+
+#[cfg(not(target_arch = "wasm32"))]
+pub use crate::backend::app::{LayerAnchor, LayerKeyboardInteractivity, LayerKind, LayerSettings};
+#[cfg(not(target_arch = "wasm32"))]
+pub use crate::backend::window_runner::{run, xdg_toplevel, EngineState};
diff --git a/src/lib.rs b/src/lib.rs
index 43d8a5a..fdab53f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,7 @@
+// Modules under `cfg(not(target_arch = "wasm32"))` are the native shell and
+// renderer (Wayland, Vulkan, the compositor IPC, file dialogs). Everything
+// else builds for the browser too: `cargo check --lib --target
+// wasm32-unknown-unknown` is the check.
pub mod color;
pub mod widget;
pub mod config;
@@ -5,7 +9,9 @@ pub mod input;
pub mod history;
pub mod layout;
pub mod relief_spec;
+#[cfg(not(target_arch = "wasm32"))]
pub mod wayland;
+#[cfg(not(target_arch = "wasm32"))]
pub mod protocol;
pub mod engine;
pub mod scale;
@@ -14,11 +20,15 @@ pub mod backend;
pub mod context;
pub mod draw;
pub mod scene;
+#[cfg(not(target_arch = "wasm32"))]
pub mod file_dialog;
pub mod icon;
+#[cfg(not(target_arch = "wasm32"))]
pub mod ipc;
+#[cfg(not(target_arch = "wasm32"))]
pub mod mcp;
pub mod motion;
+#[cfg(not(target_arch = "wasm32"))]
pub mod vk;
pub mod colors {
diff --git a/src/motion.rs b/src/motion.rs
index 7554c36..de298e6 100644
--- a/src/motion.rs
+++ b/src/motion.rs
@@ -19,7 +19,8 @@
//! (`0`/`off` or `1`/`on`) overrides the file for one process, for testing.
use std::sync::Mutex;
-use std::time::{Duration, Instant};
+use std::time::Duration;
+use web_time::Instant;
/// Where the switch lives. Shared with `cce-power-apply`, the writer.
pub const STATE_PATH: &str = "/run/cce/animations";
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 9e92fd8..5d6ef1b 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -188,7 +188,7 @@ pub struct TreeList {
pub deleted_key_path: Option<String>,
pub edit_box: crate::widget::Adapted<TextBox>,
pub editing_key_idx: Option<usize>,
- pub double_click_timer: Option<(std::time::Instant, usize)>,
+ pub double_click_timer: Option<(web_time::Instant, usize)>,
pub rename_request: Option<(String, String)>,
}
@@ -459,7 +459,7 @@ impl TreeList {
let item = self.items[row_idx].clone();
let mut is_double = false;
- let now = std::time::Instant::now();
+ let now = web_time::Instant::now();
if let Some((prev_time, prev_row)) = self.double_click_timer {
if prev_row == row_idx && now.duration_since(prev_time).as_millis() < 300 {
is_double = true;
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 4a8cbe6..3f5c9e9 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -448,7 +448,7 @@ pub mod context_menu {
struct Turning {
/// The plate as it stood when the turn began: its size and its rows.
from: Box<ContextMenuState>,
- start: std::time::Instant,
+ start: web_time::Instant,
/// +1 forward (the page comes in from the right, where `›` points),
/// -1 back.
dir: f32,
@@ -514,7 +514,7 @@ pub mod context_menu {
/// When the menu was last hidden: a page shown in the same moment
/// turns from it, as a host that closes one menu and shows the next
/// in one dispatch means it to.
- hidden_at: Option<std::time::Instant>,
+ hidden_at: Option<web_time::Instant>,
/// The slider row a press took hold of, until the release.
pub slider_drag: Option<usize>,
/// A trackpad's leftover fraction of a wheel notch.
@@ -684,7 +684,7 @@ pub mod context_menu {
self.turned = true;
self.turning = from.map(|from| Turning {
from,
- start: std::time::Instant::now(),
+ start: web_time::Instant::now(),
dir: if back.is_some() { 1.0 } else { -1.0 },
});
if let Some(title) = back {
@@ -733,7 +733,7 @@ pub mod context_menu {
from.hovered_item = None;
from.w = w;
from.h = h;
- self.turning = Some(Turning { from: Box::new(from), start: std::time::Instant::now(), dir: if forward { 1.0 } else { -1.0 } });
+ self.turning = Some(Turning { from: Box::new(from), start: web_time::Instant::now(), dir: if forward { 1.0 } else { -1.0 } });
}
/// How far the turn in progress has gone, eased, 0..1; `None` when
@@ -1040,7 +1040,7 @@ pub mod context_menu {
self.back_hovered = false;
self.turn = None;
self.turning = None;
- self.hidden_at = Some(std::time::Instant::now());
+ self.hidden_at = Some(web_time::Instant::now());
}
pub fn hit_test(&self, px: f32, py: f32) -> bool {
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 01b2f7e..48fc3d9 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -308,7 +308,7 @@ pub struct Graph {
/// the first press's timer never survived to the second press, so
/// double-click detection could not fire at all. Same id-keyed survival
/// as `selected_id` and the hovered-port remap.
- double_click_timer: Option<(std::time::Instant, String)>,
+ double_click_timer: Option<(web_time::Instant, String)>,
grid_snap_enabled: bool,
node_geom_toggled: Option<(usize, bool)>,
@@ -1330,7 +1330,7 @@ impl Graph {
}
if let Some((nx, ny, nw, nh)) = self.node_rect(i) {
if px >= nx && px < nx + nw && py >= ny && py < ny + nh {
- let now = std::time::Instant::now();
+ let now = web_time::Instant::now();
let clicked_id = self.nodes[i].id.clone();
if let Some((prev_time, prev_id)) = self.double_click_timer.take() {
if prev_id == clicked_id && now.duration_since(prev_time) < std::time::Duration::from_millis(500) {
diff --git a/src/widget/doc_editor/mod.rs b/src/widget/doc_editor/mod.rs
index 6b4a992..794fe97 100644
--- a/src/widget/doc_editor/mod.rs
+++ b/src/widget/doc_editor/mod.rs
@@ -25,7 +25,8 @@ pub mod buffer;
pub mod layout;
pub mod preview;
-use std::time::{Duration, Instant};
+use std::time::Duration;
+use web_time::Instant;
pub use buffer::{Buffer, EditKind, Pos};
pub use layout::EditorTheme;
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 56b8fff..b088d6c 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -654,31 +654,7 @@ impl Input for ColorSelector {
self.command.clone()
};
- // Ask the compositor to open the picker at this control instead of
- // its remembered position: the pointer is on the swatch right now,
- // so its location IS the control's location. One-shot, best-effort
- // (`place-next` consumed at the picker's map; ignored off-cce).
- if let Ok(reply) = crate::ipc::send_command("cce", "pointer-location") {
- let mut px = None;
- let mut py = None;
- for tok in reply.split_whitespace() {
- if let Some(v) = tok.strip_prefix("x=") {
- px = v.parse::<f64>().ok();
- } else if let Some(v) = tok.strip_prefix("y=") {
- py = v.parse::<f64>().ok();
- }
- }
- if let (Some(x), Some(y)) = (px, py) {
- let app_id = std::path::Path::new(&self.command)
- .file_name()
- .map(|n| n.to_string_lossy().into_owned())
- .unwrap_or_else(|| self.command.clone());
- let _ = crate::ipc::send_command(
- "cce",
- &format!("place-next {} {:.0} {:.0}", app_id, x, y),
- );
- }
- }
+ place_picker_at_pointer(&self.command);
let mut cmd = std::process::Command::new(&cmd_path);
cmd.arg(&hex);
@@ -828,6 +804,39 @@ fn parse_hex(s: &str) -> Option<[u8; 4]> {
crate::color::parse_hex_bytes(s)
}
+/// Ask the compositor to open the picker at this control instead of its
+/// remembered position. Native only: in a browser there is no compositor to ask.
+#[cfg(not(target_arch = "wasm32"))]
+fn place_picker_at_pointer(command: &str) {
+ // The pointer is on the swatch right now, so its location IS the
+ // control's location. One-shot, best-effort (`place-next` consumed at
+ // the picker's map; ignored off-cce).
+ if let Ok(reply) = crate::ipc::send_command("cce", "pointer-location") {
+ let mut px = None;
+ let mut py = None;
+ for tok in reply.split_whitespace() {
+ if let Some(v) = tok.strip_prefix("x=") {
+ px = v.parse::<f64>().ok();
+ } else if let Some(v) = tok.strip_prefix("y=") {
+ py = v.parse::<f64>().ok();
+ }
+ }
+ if let (Some(x), Some(y)) = (px, py) {
+ let app_id = std::path::Path::new(&command)
+ .file_name()
+ .map(|n| n.to_string_lossy().into_owned())
+ .unwrap_or_else(|| command.to_string());
+ let _ = crate::ipc::send_command(
+ "cce",
+ &format!("place-next {} {:.0} {:.0}", app_id, x, y),
+ );
+ }
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+fn place_picker_at_pointer(_command: &str) {}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1086,5 +1095,3 @@ mod tests {
}
}
-
-
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 0dbb62a..d770b50 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -218,7 +218,7 @@ pub struct Dropdown {
/// close. `closing` keeps `open` (and the shrinking popover) alive while
/// everything interactive gates on `!closing`.
anim_from: f32,
- anim_start: Option<std::time::Instant>,
+ anim_start: Option<web_time::Instant>,
closing: bool,
/// Per-frame SNAPSHOT of the wall-clock progress, refreshed in `tick`
/// (before each render) and on every routed event. All geometry readers —
@@ -366,7 +366,7 @@ impl Dropdown {
fn begin_open(&mut self) {
self.anim_from = self.anim_progress_now();
- self.anim_start = Some(std::time::Instant::now());
+ self.anim_start = Some(web_time::Instant::now());
self.open = true;
self.closing = false;
self.anim_snap = self.anim_progress_now();
@@ -377,7 +377,7 @@ impl Dropdown {
return;
}
self.anim_from = self.anim_progress_now();
- self.anim_start = Some(std::time::Instant::now());
+ self.anim_start = Some(web_time::Instant::now());
self.closing = true;
self.anim_snap = self.anim_progress_now();
}
@@ -409,7 +409,7 @@ impl Dropdown {
/// wall clock).
#[cfg(test)]
fn land_anim_for_test(&mut self) {
- self.anim_start = Some(std::time::Instant::now() - std::time::Duration::from_secs(1));
+ self.anim_start = Some(web_time::Instant::now() - std::time::Duration::from_secs(1));
self.settle_anim();
}
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index f5d2dbb..67c9430 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -126,7 +126,7 @@ pub struct Ramp {
/// the last scroll-event instant: when the event stream stops, the tick
/// keeps the latched key coasting with exponential decay.
scroll_vel: (f32, f32),
- last_key_scroll: Option<std::time::Instant>,
+ last_key_scroll: Option<web_time::Instant>,
// Child controls for key editing & deletion. The key pad is a 2-axis
// slider driving the selected key's position (x) and value (y).
@@ -1674,7 +1674,7 @@ impl Input for Ramp {
// applied delta over inter-event time. A leisurely
// wheel produces negligible velocity (big gaps clamp
// to 0.1s); fast trackpad streams build real speed.
- let now = std::time::Instant::now();
+ let now = web_time::Instant::now();
let dt_ev = self
.last_key_scroll
.map(|t| now.duration_since(t).as_secs_f32())
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 4c7d38a..c580a1a 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -48,7 +48,7 @@ pub struct Slider {
/// stream stops (fingers lifted), `tick` keeps the value coasting with
/// exponential decay instead of stopping dead.
scroll_vel: f32,
- last_wheel: Option<std::time::Instant>,
+ last_wheel: Option<web_time::Instant>,
/// Keyboard focus (FocusIn / FocusOut): the band lights in the highlight;
/// the arrows adjust, Home / End go to the ends, Enter opens the readout.
focused: bool,
@@ -474,7 +474,7 @@ impl Input for Slider {
// negligible velocity (big gaps clamp to 0.1s); fast
// trackpad streams build real speed. Hitting an end
// stops dead — no glide pinned at the bounds.
- let now = std::time::Instant::now();
+ let now = web_time::Instant::now();
let idt = self
.last_wheel
.map_or(0.1, |l| now.duration_since(l).as_secs_f32())
diff --git a/src/widget/line_edit.rs b/src/widget/line_edit.rs
index 96ff869..41dc911 100644
--- a/src/widget/line_edit.rs
+++ b/src/widget/line_edit.rs
@@ -19,7 +19,8 @@
//!
//! [`TextBox`]: super::TextBox
-use std::time::{Duration, Instant};
+use std::time::Duration;
+use web_time::Instant;
use crate::history::History;
use crate::widget::{ElementState, Key, KeyEvent, NamedKey};
diff --git a/src/widget/scroll_motion.rs b/src/widget/scroll_motion.rs
index 52f2292..2413545 100644
--- a/src/widget/scroll_motion.rs
+++ b/src/widget/scroll_motion.rs
@@ -41,7 +41,7 @@
//! ```
use std::sync::atomic::{AtomicU8, Ordering};
-use std::time::Instant;
+use web_time::Instant;
use crate::widget::MouseScrollDelta;
diff --git a/src/widget/side_swipe.rs b/src/widget/side_swipe.rs
index 295e99a..8a0a4fe 100644
--- a/src/widget/side_swipe.rs
+++ b/src/widget/side_swipe.rs
@@ -27,7 +27,8 @@
use crate::widget::scroll_motion::{current_scroll_phase, ScrollPhase};
use crate::widget::MouseScrollDelta;
-use std::time::{Duration, Instant};
+use std::time::Duration;
+use web_time::Instant;
/// How far the fingers travel to the side, in the runner's pixel delta,
/// before a swipe is a page turn. A deliberate flick covers this in two or