GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: DemoApp — the demo rewritten as the reference Application (Phase 6ad)
src/main.rs was never the "reference Application" the docs claimed: it was a
1925-line fossil predating the engine — a raw Wayland client with its own
handler impls, wgpu state, and hand-copied tessellators. Replaced by a small
teaching-commented widget gallery on the full target architecture:
- display-list frame (geometry + text prims, display_list_text = true)
- layout as a plain Arena<LayoutBox> tree solved by compute_layout (shrink
demonstrated for min-width rows)
- routed events: one Event per handler through propagate_event per root; the
KeyInput short-circuit rule and state-gated drain_widget_changes plumbing
- ui_context-only popover registration + the in-frame popover draw
- dissolved-root window plate prims and drag_allowed_at window dragging
Live-verified: button click, toggle (app-state re-assert), slider wheel nudge,
dropdown popover open/select with the occlusion clamp visible. Slider footgun
recorded: set_value takes the NORMALIZED 0..1 value; with_range only scales
get_scaled_value.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 7 +-
docs/rfc-core-rebuild.md | 25 +-
src/main.rs | 2222 ++++++++--------------------------------------
3 files changed, 408 insertions(+), 1846 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 0096234..066ef2e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,9 +21,10 @@ set outright.
**glyphon** for text. There is no HTML/DOM — the UI is GPU primitives (quads, rounded rects with
per-corner radii, vectors with caps, arcs, circles, bevels). Tessellators live in
`backend/window_runner.rs` and are re-exported through `src/engine.rs`.
-- It is **both a library and a binary.** `src/lib.rs` is the toolkit; `src/main.rs` is a
- standalone demo/reference `AppState` (see `demo_positions`) — a runnable example of the
- `Application` contract, not something clients use.
+- It is **both a library and a binary.** `src/lib.rs` is the toolkit; `src/main.rs` is
+ `DemoApp`, the reference `Application` — a small widget gallery on the Phase 6 target
+ architecture (display-list frame, scene-solver layout, routed events, in-frame
+ popovers). Copy it when starting a new client.
## Build, test, run
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index ebe6592..f0897ae 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -1206,11 +1206,26 @@ Constraint respected: **each crate still builds standalone** — the new core is
widget state instead (Enter→ApplyValue now checks the value editor was
editing when the key arrived). Keyboard flows not headlessly drivable —
user spot-check (typing, Enter-apply, tree search, keybind recording).
- - **Still to do:** the demo
- (`cce-ui/src/main.rs`) as the reference `Application`. Delete the legacy
- `view*`/`text_items` paths, the per-widget text getters, the write-only
- global popovers registry, and finally `Element` + `Adapted` once the last
- app is across.
+ - **6ad — the demo rewritten as `DemoApp`, the reference `Application`. DONE
+ (live-verified: button click, toggle with app-state re-assert, slider wheel
+ nudge, dropdown popover open/select with the occlusion clamp visibly
+ working, all through routed dispatch).** `src/main.rs` had never been the
+ "reference Application" the docs claimed — it was a 1925-line fossil
+ predating the engine entirely: a raw Wayland client with its own
+ CompositorHandler/SeatHandler impls, its own wgpu state, and hand-copied
+ tessellators. Replaced by ~450 teaching-commented lines on the full target
+ architecture: display-list frame + display_list_text, solver-driven layout
+ (with `shrink` demonstrated for min-width rows), routed events with the
+ KeyInput short-circuit rule and state-gated `drain_widget_changes`
+ plumbing, ui_context-only popover registration with the in-frame draw, the
+ dissolved-root window plate, and `drag_allowed_at` window dragging. API
+ footgun surfaced for the log: `Slider::set_value` takes the NORMALIZED
+ 0..1 value (`with_range` only scales `get_scaled_value`) — passing a
+ ranged value silently clamps to 1.0.
+ - **Still to do:** the legacy deletion — the `view*`/`text_items` paths, the
+ per-widget text getters, the write-only global popovers registry, and
+ finally `Element` + `Adapted` (TreeList converting to narrow traits with
+ it).
Order rationale: each phase is independently valuable and reversible, and no phase requires the
next to compile. Phase 0 can land immediately regardless of the rest.
diff --git a/src/main.rs b/src/main.rs
index ae62044..3de4ff7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1925 +1,471 @@
-use cce_ui::widget::{
- Button, Checkbox, ContentBg, Header, Panel, ProgressBar, RangeSlider, Sidebar, Slider, Spinbox,
- StatusBar, TextLabel, Toggle, Element, JsonLayoutWidget, JsonLayoutConfig,
-};
-
-use glyphon::{
- Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
- TextBounds, TextRenderer, Viewport,
-};
-
-use smithay_client_toolkit::{
- compositor::{CompositorHandler, CompositorState},
- delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
- delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
- registry::{ProvidesRegistryState, RegistryState},
- output::{OutputHandler, OutputState},
- seat::{
- keyboard::KeyboardHandler,
- pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
- Capability, SeatHandler, SeatState,
- },
- shell::{
- xdg::{
- window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
- XdgShell,
- },
- WaylandSurface,
- },
- shm::{Shm, ShmHandler},
+//! The reference `Application` (Phase 6ad) — a small widget gallery on the target
+//! architecture, end to end. This is the file to copy when starting a new `cce-*` client.
+//!
+//! The shape every migrated app shares:
+//!
+//! 1. **One paint path.** The whole frame — geometry AND text — is built in
+//! [`Application::display_list`] as prims on a [`PaintCtx`], with
+//! [`Application::display_list_text`] returning `true`. There is no `view*`/`text_items`
+//! pair, no app-side `FontSystem`, no glyphon buffers: text is a `Prim::Text` shaped by
+//! the engine's shared cache.
+//! 2. **Layout via the scene solver.** The frame is a plain `Arena<LayoutBox>` tree the
+//! app builds and solves with [`compute_layout`]; widgets get their rects from the
+//! solved leaves. No container widgets, no hand-summed offsets.
+//! 3. **Routed events.** Each input handler builds one [`Event`] and routes it through
+//! `UiContext::propagate_event` per widget root. The router owns press hit-gating,
+//! Enter/Leave synthesis, drag-target recording, and KeyInput-to-focused delivery;
+//! the app keeps only state-gated `take_*` plumbing.
+//! 4. **No embedded bases.** Widgets are app-owned values (all [`Adapted`]); the window
+//! plate is prims, not a `Backplate`; popovers draw INTO the frame (there is no popup
+//! surface); app state — not any widget tree — is the source of truth.
+
+use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use cce_ui::scene::arena::Arena;
+use cce_ui::scene::layout::{
+ compute_layout, CrossAlign, LayoutBox, Length, Rect, Size as LSize, Style,
};
-use wayland_client::{
- globals::registry_queue_init,
- protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
- Connection, QueueHandle, Proxy,
+use cce_ui::scene::paint::{DisplayList, PaintCtx};
+use cce_ui::widget::{
+ Adapted, Button, Dropdown, Element, ElementState, Event, KeyEvent, MouseButton,
+ MouseScrollDelta, Slider, TextBox, Toggle,
};
-use calloop::EventLoop;
-use calloop_wayland_source::WaylandSource;
-
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct Vertex {
- position: [f32; 2],
- color: [f32; 4],
- clip_circle: [f32; 3],
-}
-
-impl Vertex {
- const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
- 0 => Float32x2,
- 1 => Float32x4,
- 2 => Float32x3,
- ];
-
- fn desc() -> wgpu::VertexBufferLayout<'static> {
- wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &Self::ATTRIBS,
- }
- }
-}
-
-fn circle_vertices(
- cx: f32, cy: f32, r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- segments: usize,
-) -> Vec<Vertex> {
- let mut verts = Vec::with_capacity(segments * 3);
- for i in 0..segments {
- let a1 = i as f32 * 2.0 * std::f32::consts::PI / segments as f32;
- let a2 = (i + 1) as f32 * 2.0 * std::f32::consts::PI / segments as f32;
- let x1 = cx + r * a1.cos();
- let y1 = cy + r * a1.sin();
- let x2 = cx + r * a2.cos();
- let y2 = cy + r * a2.sin();
-
- verts.push(Vertex {
- position: [(cx / sw) * 2.0 - 1.0, 1.0 - (cy / sh) * 2.0],
- color,
- clip_circle: [0.0, 0.0, 0.0],
- });
- verts.push(Vertex {
- position: [(x1 / sw) * 2.0 - 1.0, 1.0 - (y1 / sh) * 2.0],
- color,
- clip_circle: [0.0, 0.0, 0.0],
- });
- verts.push(Vertex {
- position: [(x2 / sw) * 2.0 - 1.0, 1.0 - (y2 / sh) * 2.0],
- color,
- clip_circle: [0.0, 0.0, 0.0],
- });
- }
- verts
-}
-
-fn quad_vertices(
- x: f32, y: f32, w: f32, h: f32,
- surface_w: f32, surface_h: f32,
- color: [f32; 4],
-) -> [Vertex; 6] {
- let x0 = (x / surface_w) * 2.0 - 1.0;
- let y0 = 1.0 - (y / surface_h) * 2.0;
- let x1 = ((x + w) / surface_w) * 2.0 - 1.0;
- let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
+use wayland_client::QueueHandle;
- [
- Vertex { position: [x0, y0], color, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x1, y1], color, clip_circle: [0.0, 0.0, 0.0] },
- Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
- ]
+#[derive(Debug, Clone)]
+enum DemoMessage {
+ Exit,
}
-fn rounded_rect_vertices_corners(
- x: f32, y: f32, ww: f32, h: f32,
- r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
- corners: (bool, bool, bool, bool),
-) -> Vec<Vertex> {
- let mut verts = Vec::new();
- let r = r.min(ww * 0.5).min(h * 0.5);
+struct DemoApp {
+ // ── Widgets: app-owned values on the narrow-trait adapter. Their addresses must be
+ // stable across frames (plain struct fields, not Vec elements): the UiContext
+ // registry and the router's drag-target bookkeeping hold pointers to them.
+ button: Adapted<Button>,
+ toggle: Adapted<Toggle>,
+ slider: Adapted<Slider>,
+ name_box: Adapted<TextBox>,
+ theme_dropdown: Adapted<Dropdown>,
+
+ // ── App state: the source of truth. Widgets are re-asserted from it every rebuild
+ // (`set_toggled` below); `take_*` changes flow back into it, never the reverse.
+ toggle_on: bool,
+ clicks: u32,
+ status: String,
- let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
- let x0 = qx;
- let y0 = qy;
- let x1 = qx + qw;
- let y1 = qy + qh;
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
- let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
- let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
-
- let clip_circle = [0.0, 0.0, 0.0];
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
- };
-
- if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
- push_quad(&mut verts, x, y, ww, h);
- return verts;
- }
-
- push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
- push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
- push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
-
- let corner_configs = [
- (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
- (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
- (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
- (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
- ];
-
- let segments = 16;
- for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
- if is_rounded {
- for i in 0..segments {
- let theta1 = start + (i as f32) * (end - start) / (segments as f32);
- let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
-
- let x0 = cx;
- let y0 = cy;
- let x1 = cx + r * theta1.cos();
- let y1 = cy + r * theta1.sin();
- let x2 = cx + r * theta2.cos();
- let y2 = cy + r * theta2.sin();
-
- let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
- let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
- let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
- let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
- let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
- let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-
- let clip_circle = [0.0, 0.0, 0.0];
- verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
- verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
- verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
- }
- } else {
- push_quad(&mut verts, sqx, sqy, r, r);
- }
- }
-
- verts
-}
-
-#[allow(dead_code)]
-fn rounded_rect_vertices(
- x: f32, y: f32, ww: f32, h: f32,
- r: f32,
- sw: f32, sh: f32,
- color: [f32; 4],
-) -> Vec<Vertex> {
- rounded_rect_vertices_corners(x, y, ww, h, r, sw, sh, color, (true, true, true, true))
+ ui_context: cce_ui::context::UiContext,
+ width: u32,
+ height: u32,
+ scale_factor: f64,
+ needs_rebuild: bool,
+ widgets_registered: bool,
+ title_rect: Rect,
+ status_rect: Rect,
}
-fn widget_vertices(w: &dyn Element, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, ww, h) = w.rect();
- let corners = w.rounded_corners();
- if corners != (false, false, false, false) {
- rounded_rect_vertices_corners(x, y, ww, h, w.corner_radius(), sw, sh, w.color(), corners)
- } else {
- quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
+impl DemoApp {
+ /// The widget roots, in paint order (events route over the same list).
+ fn roots(&mut self) -> [*mut (dyn Element + 'static); 5] {
+ [
+ self.button.as_ptr_mut(),
+ self.toggle.as_ptr_mut(),
+ self.slider.as_ptr_mut(),
+ self.name_box.as_ptr_mut(),
+ self.theme_dropdown.as_ptr_mut(),
+ ]
}
-}
-
-fn extra_quad_vertices(
- w: &dyn Element,
- qx: f32, qy: f32, qw: f32, qh: f32,
- sw: f32, sh: f32,
- qc: [f32; 4],
-) -> Vec<Vertex> {
- let mut corners = w.rounded_corners();
- let mut r = w.corner_radius();
- let (mut wx, mut wy, mut ww, mut wh) = w.rect();
- let mut top_room = cce_ui::widget::label_offset(w);
- if let Some(mc) = w.as_any().downcast_ref::<cce_ui::widget::input::MultiControl>() {
- // check add_button
- let (bx, by, b_w, b_h) = mc.add_button.rect();
- if qx >= bx - 0.1 && qx + qw <= bx + b_w + 0.1 && qy >= by - 0.1 && qy + qh <= by + b_h + 0.1 {
- corners = mc.add_button.rounded_corners();
- r = mc.add_button.corner_radius();
- wx = bx;
- wy = by;
- ww = b_w;
- wh = b_h;
- top_room = cce_ui::widget::label_offset(&mc.add_button);
+ /// `take_*` plumbing: translate widget changes into app state. Runs after any routed
+ /// dispatch; every check is STATE-gated, so it does not matter which propagate call
+ /// consumed the event (see the KeyInput note in `handle_key_input`).
+ fn drain_widget_changes(&mut self) {
+ if self.button.take_click() {
+ self.clicks += 1;
+ self.status = format!("Button clicked {} time(s)", self.clicks);
+ self.needs_rebuild = true;
}
- for row in &mc.rows {
- // key_input
- let (kx, ky, kw, kh) = row.key_input.rect();
- if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
- corners = row.key_input.rounded_corners();
- r = row.key_input.corner_radius();
- wx = kx;
- wy = ky;
- ww = kw;
- wh = kh;
- top_room = cce_ui::widget::label_offset(&row.key_input);
- }
- // type_dropdown
- let (tx, ty, tw, th) = row.type_dropdown.rect();
- if qx >= tx - 0.1 && qx + qw <= tx + tw + 0.1 && qy >= ty - 0.1 && qy + qh <= ty + th + 0.1 {
- corners = row.type_dropdown.rounded_corners();
- r = row.type_dropdown.corner_radius();
- wx = tx;
- wy = ty;
- ww = tw;
- wh = th;
- top_room = cce_ui::widget::label_offset(&row.type_dropdown);
- }
- // remove_button
- let (rx, ry, rw, rh) = row.remove_button.rect();
- if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
- corners = row.remove_button.rounded_corners();
- r = row.remove_button.corner_radius();
- wx = rx;
- wy = ry;
- ww = rw;
- wh = rh;
- top_room = cce_ui::widget::label_offset(&row.remove_button);
- }
- // value_widget
- let (vx, vy, v_w, v_h) = row.value_widget.rect();
- if qx >= vx - 0.1 && qx + qw <= vx + v_w + 0.1 && qy >= vy - 0.1 && qy + qh <= vy + v_h + 0.1 {
- let (sub_corners, sub_radius, sub_top_room) = match &row.value_widget {
- cce_ui::widget::input::InstancedWidget::TextBox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
- cce_ui::widget::input::InstancedWidget::Spinbox(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
- cce_ui::widget::input::InstancedWidget::Toggle(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
- cce_ui::widget::input::InstancedWidget::Slider(w) => (w.rounded_corners(), w.corner_radius(), cce_ui::widget::label_offset(w)),
- };
- corners = sub_corners;
- r = sub_radius;
- wx = vx;
- wy = vy;
- ww = v_w;
- wh = v_h;
- top_room = sub_top_room;
- }
+ if self.toggle.take_change() {
+ self.toggle_on = !self.toggle_on;
+ self.status = format!("Toggle: {}", if self.toggle_on { "on" } else { "off" });
+ self.needs_rebuild = true;
}
- }
-
- if let Some(kc) = w.as_any().downcast_ref::<cce_ui::widget::input::KeybindsControl>() {
- // check add_button
- let (bx, by, b_w, b_h) = kc.add_button.rect();
- if qx >= bx - 0.1 && qx + qw <= bx + b_w + 0.1 && qy >= by - 0.1 && qy + qh <= by + b_h + 0.1 {
- corners = kc.add_button.rounded_corners();
- r = kc.add_button.corner_radius();
- wx = bx;
- wy = by;
- ww = b_w;
- wh = b_h;
- top_room = cce_ui::widget::label_offset(&kc.add_button);
+ if self.slider.take_change() {
+ self.status = format!("Slider: {:.0}", self.slider.get_scaled_value());
+ self.needs_rebuild = true;
}
- for row in &kc.rows {
- // key_input
- let (kx, ky, kw, kh) = row.key_input.rect();
- if qx >= kx - 0.1 && qx + qw <= kx + kw + 0.1 && qy >= ky - 0.1 && qy + qh <= ky + kh + 0.1 {
- corners = row.key_input.rounded_corners();
- r = row.key_input.corner_radius();
- wx = kx;
- wy = ky;
- ww = kw;
- wh = kh;
- top_room = cce_ui::widget::label_offset(&row.key_input);
- }
- // cmd_input
- let (cx, cy, cw, ch) = row.cmd_input.rect();
- if qx >= cx - 0.1 && qx + qw <= cx + cw + 0.1 && qy >= cy - 0.1 && qy + qh <= cy + ch + 0.1 {
- corners = row.cmd_input.rounded_corners();
- r = row.cmd_input.corner_radius();
- wx = cx;
- wy = cy;
- ww = cw;
- wh = ch;
- top_room = cce_ui::widget::label_offset(&row.cmd_input);
- }
- // remove_button
- let (rx, ry, rw, rh) = row.remove_button.rect();
- if qx >= rx - 0.1 && qx + qw <= rx + rw + 0.1 && qy >= ry - 0.1 && qy + qh <= ry + rh + 0.1 {
- corners = row.remove_button.rounded_corners();
- r = row.remove_button.corner_radius();
- wx = rx;
- wy = ry;
- ww = rw;
- wh = rh;
- top_room = cce_ui::widget::label_offset(&row.remove_button);
+ if self.theme_dropdown.take_change() {
+ let idx = self.theme_dropdown.selected;
+ if let Some(opt) = self.theme_dropdown.options.get(idx) {
+ self.status = format!("Theme: {opt}");
}
+ self.needs_rebuild = true;
}
- }
-
- if corners == (false, false, false, false) {
- return quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
- }
-
- wy += top_room;
- wh -= top_room;
- let extra_corners = (
- corners.0 && qx <= wx + 1.5 && qy <= wy + 1.5,
- corners.1 && qx + qw >= wx + ww - 1.5 && qy <= wy + 1.5,
- corners.2 && qx + qw >= wx + ww - 1.5 && qy + qh >= wy + wh - 1.5,
- corners.3 && qx <= wx + 1.5 && qy + qh >= wy + wh - 1.5,
- );
-
- rounded_rect_vertices_corners(qx, qy, qw, qh, r, sw, sh, qc, extra_corners)
-}
-
-#[derive(Hash, PartialEq, Eq, Clone)]
-struct BufferCacheKey {
- text: String,
- size_milli: u32,
- font: Option<String>,
-}
-
-std::thread_local! {
- static BUFFER_CACHE: std::cell::RefCell<std::collections::HashMap<BufferCacheKey, Buffer>> = std::cell::RefCell::new(std::collections::HashMap::new());
-}
-
-fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
- make_text_buffer_with_font(font_system, text, size, None)
-}
-
-fn make_text_buffer_with_font(font_system: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
- let scale = cce_ui::scale::scale_factor();
- let mut font_size = size;
- let mut family_name = None;
-
- if let Some(font_str) = font {
- let (parsed_family, parsed_size) = cce_ui::layout::parse_font_string(font_str);
- if let Some(ps) = parsed_size {
- font_size = ps;
+ if self.name_box.take_change() {
+ self.status = format!("Name: {}", self.name_box.text);
+ self.needs_rebuild = true;
}
- family_name = Some(parsed_family);
}
-
- let physical_size = font_size * scale;
- let size_key = (physical_size * 1000.0).round() as u32;
-
- let key = BufferCacheKey {
- text: text.to_string(),
- size_milli: size_key,
- font: family_name.clone(),
- };
-
- let cached = BUFFER_CACHE.with(|cache| {
- cache.borrow().get(&key).cloned()
- });
-
- if let Some(buf) = cached {
- return buf;
- }
-
- let metrics = Metrics::new(physical_size, physical_size * 1.0);
- let mut buffer = Buffer::new(font_system, metrics);
- let mut attrs = Attrs::new();
- if let Some(font_name) = family_name.as_deref() {
- let family = match font_name {
- "monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
- "sans-serif" => glyphon::Family::SansSerif,
- "serif" => glyphon::Family::Serif,
- _ => glyphon::Family::Name(font_name),
- };
- attrs = attrs.family(family);
- }
- buffer.set_text(font_system, text, attrs, glyphon::Shaping::Advanced);
- buffer.shape_until_scroll(font_system, true);
-
- BUFFER_CACHE.with(|cache| {
- cache.borrow_mut().insert(key, buffer.clone());
- });
-
- buffer
}
+impl Application for DemoApp {
+ type Message = DemoMessage;
-struct State {
- surface: wgpu::Surface<'static>,
- device: wgpu::Device,
- queue: wgpu::Queue,
- config: wgpu::SurfaceConfiguration,
- render_pipeline: wgpu::RenderPipeline,
- vertex_buffer: wgpu::Buffer,
- vertex_count: u32,
-
- widgets: Vec<Box<dyn Element>>,
- positions: Vec<(f32, f32, f32, f32)>,
-
- font_system: FontSystem,
- swash_cache: SwashCache,
- text_atlas: TextAtlas,
- text_renderer: TextRenderer,
- text_viewport: Viewport,
-
- label_buffer: Buffer,
- status_buffer: Buffer,
-
- drag_widget: Option<usize>,
- focused_widget: Option<usize>,
- click_count: u32,
-
- cursor_x: f32,
- cursor_y: f32,
-
- width: f32,
- height: f32,
- physical_width: u32,
- physical_height: u32,
- scale: f64,
- json_layout: Option<JsonLayoutWidget>,
- layout_mode: bool,
- ui_context: cce_ui::context::UiContext,
-}
-
-impl State {
- async fn new(
- wayland_handle: &'static cce_ui::wayland::WaylandSurfaceHandle,
- pw: u32,
- ph: u32,
- scale: f64,
- json_layout_config: Option<JsonLayoutConfig>,
+ fn new(
+ _qh: &QueueHandle<EngineState<Self>>,
+ _sender: calloop::channel::Sender<Self::Message>,
) -> Self {
- cce_ui::scale::set_scale_factor(scale as f32);
- let lw = pw as f32 / scale as f32;
- let lh = ph as f32 / scale as f32;
- let sw = lw;
- let sh = lh;
-
- let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
- backends: wgpu::Backends::VULKAN,
- ..Default::default()
- });
-
- let surface = instance.create_surface(wayland_handle).unwrap();
-
- let adapter = instance
- .request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::HighPerformance,
- compatible_surface: Some(&surface),
- force_fallback_adapter: false,
- })
- .await
- .unwrap();
-
- let (device, queue) = adapter
- .request_device(
- &wgpu::DeviceDescriptor {
- label: None,
- required_features: wgpu::Features::empty(),
- required_limits: wgpu::Limits::default(),
- memory_hints: wgpu::MemoryHints::MemoryUsage,
- },
- None,
- )
- .await
- .unwrap();
-
- let surface_caps = surface.get_capabilities(&adapter);
- let surface_format = surface_caps
- .formats
- .iter()
- .copied()
- .find(|f| f.is_srgb())
- .unwrap_or(surface_caps.formats[0]);
- let config = wgpu::SurfaceConfiguration {
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
- format: surface_format,
- width: pw,
- height: ph,
- present_mode: wgpu::PresentMode::Fifo,
- alpha_mode: surface_caps.alpha_modes[0],
- view_formats: vec![],
- desired_maximum_frame_latency: 2,
- };
- surface.configure(&device, &config);
-
- let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
- let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("Render Pipeline Layout"),
- bind_group_layouts: &[],
- push_constant_ranges: &[],
- });
- let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("Render Pipeline"),
- layout: Some(&render_pipeline_layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: Some("vs_main"),
- buffers: &[Vertex::desc()],
- compilation_options: Default::default(),
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: Some("fs_main"),
- targets: &[Some(wgpu::ColorTargetState {
- format: config.format,
- blend: Some(wgpu::BlendState::ALPHA_BLENDING),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- compilation_options: Default::default(),
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- strip_index_format: None,
- front_face: wgpu::FrontFace::Ccw,
- cull_mode: None,
- unclipped_depth: false,
- polygon_mode: wgpu::PolygonMode::Fill,
- conservative: false,
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState {
- count: 1,
- mask: !0,
- alpha_to_coverage_enabled: false,
- },
- multiview: None,
- cache: None,
- });
-
- let mut font_system = cce_ui::create_font_system();
- let swash_cache = SwashCache::new();
- let cache = Cache::new(&device);
- let viewport = Viewport::new(&device, &cache);
- let mut text_atlas = TextAtlas::new(&device, &queue, &cache, surface_format);
- let text_renderer =
- TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
-
- let label_buffer = make_text_buffer(&mut font_system, "Design System Playground", 16.0);
- let status_buffer = make_text_buffer(&mut font_system, "Ready", 12.0);
-
- let text_viewport = viewport;
-
- let layout_mode = json_layout_config.is_some();
- let mut widgets: Vec<Box<dyn Element>> = Vec::new();
- let mut positions = Vec::new();
- let json_layout = if let Some(ref config) = json_layout_config {
- let mut jl = JsonLayoutWidget::new(config);
- jl.set_rect(0.0, 0.0, sw, sh);
- positions.push((0.0, 0.0, sw, sh));
- Some(jl)
- } else {
- widgets = vec![
- Box::new(Header::new()),
- Box::new(Sidebar::new(60.0)),
- Box::new(ContentBg::new()),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button A")),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button B")),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button C")),
- Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0).with_label("Click Me")),
- Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0).with_label("Reset")),
- Box::new(Checkbox::new()),
- Box::new(Toggle::new()),
- Box::new(ProgressBar::new(0.65)),
- Box::new(Slider::new()),
- Box::new(Spinbox::new(0, -10, 10, 1)),
- Box::new(RangeSlider::new()),
- Box::new(StatusBar::new()),
- ];
- positions = demo_positions(sw, sh);
- None
- };
-
- let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: 1,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let mut state = Self {
- surface,
- device,
- queue,
- config,
- render_pipeline,
- vertex_buffer,
- vertex_count: 0,
- widgets,
- positions,
- font_system,
- swash_cache,
- text_atlas,
- text_renderer,
- text_viewport,
- label_buffer,
- status_buffer,
- drag_widget: None,
- focused_widget: None,
- click_count: 0,
- cursor_x: 0.0,
- cursor_y: 0.0,
- width: lw,
- height: lh,
- physical_width: pw,
- physical_height: ph,
- scale,
- json_layout,
- layout_mode,
+ cce_ui::scale::set_scale_factor(1.0);
+ Self {
+ button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Click me"),
+ toggle: Toggle::new(),
+ // Slider `value` is NORMALIZED 0..1; `with_range` only scales the readout
+ // (`get_scaled_value`). Wheel nudging is an explicit opt-in.
+ slider: Slider::new().with_range(0.0, 100.0).with_value(0.4).with_scroll(true),
+ name_box: TextBox::new(String::new()).with_placeholder("Type a name..."),
+ theme_dropdown: Dropdown::new(
+ vec!["Forest".into(), "Ocean".into(), "Ember".into()],
+ 0,
+ ),
+ toggle_on: false,
+ clicks: 0,
+ status: "Ready.".to_string(),
ui_context: cce_ui::context::UiContext::new(),
- };
-
- state.apply_layout();
- state.upload_vertices();
- state
- }
-
- fn apply_layout(&mut self) {
- if self.layout_mode {
- if let Some(jl) = &mut self.json_layout {
- cce_ui::scale::set_scale_factor(self.scale as f32);
- jl.set_rect(0.0, 0.0, self.width, self.height);
- }
- } else {
- for (i, pos) in self.positions.iter().enumerate() {
- if let Some(widget) = self.widgets.get_mut(i) {
- if widget.is_dragging() {
- continue;
- }
- let (x, y, w, h) = *pos;
- widget.set_rect(x, y, w, h);
- }
- }
+ width: 560,
+ height: 420,
+ scale_factor: 1.0,
+ needs_rebuild: true,
+ widgets_registered: false,
+ title_rect: Rect::ZERO,
+ status_rect: Rect::ZERO,
}
}
- fn collect_vertices(&self) -> Vec<Vertex> {
- let sw = self.width;
- let sh = self.height;
- let mut verts = Vec::new();
- if self.layout_mode {
- verts.extend(quad_vertices(0.0, 0.0, sw, sh, sw, sh, [0.05, 0.05, 0.08, 1.0]));
- if let Some(jl) = &self.json_layout {
- verts.extend(widget_vertices(jl, sw, sh));
- for (qx, qy, qw, qh, qc) in jl.extra_quads() {
- verts.extend(extra_quad_vertices(jl, qx, qy, qw, qh, sw, sh, qc));
- }
- for (cx, cy, r, qc) in jl.extra_circles() {
- verts.extend(circle_vertices(cx, cy, r, sw, sh, qc, 16));
- }
- }
- } else {
- let mut draw_order: Vec<usize> = (0..self.widgets.len()).collect();
- draw_order.sort_by_key(|&i| self.widgets[i].z_index());
- for &i in &draw_order {
- let w = &self.widgets[i];
- verts.extend(widget_vertices(w.as_ref(), sw, sh));
- for (qx, qy, qw, qh, qc) in w.extra_quads() {
- verts.extend(extra_quad_vertices(w.as_ref(), qx, qy, qw, qh, sw, sh, qc));
- }
- for (cx, cy, r, qc) in w.extra_circles() {
- verts.extend(circle_vertices(cx, cy, r, sw, sh, qc, 16));
- }
- }
-
- // Draw popover quads on top
- let mut popover_pc = cce_ui::layout::PopoverCollector::new();
- for &i in &draw_order {
- let w = &self.widgets[i];
- if w.popover_rect().is_some() {
- w.render_popover(&mut popover_pc);
- }
- }
- for (qc, qx, qy, qw, qh) in popover_pc.rects {
- verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
- }
-
- if cce_ui::widget::context_menu::is_visible() {
- for (qx, qy, qw, qh, qc) in cce_ui::widget::context_menu::extra_quads() {
- verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
- }
- }
+ fn settings(&self) -> WindowSettings {
+ WindowSettings {
+ title: "cce-ui reference gallery".to_string(),
+ app_id: "cce-ui-demo".to_string(),
+ width: 560,
+ height: 420,
+ fullscreen: false,
+ min_size: Some((360, 300)),
}
- verts
}
- fn upload_vertices(&mut self) {
- let verts = self.collect_vertices();
- self.vertex_count = verts.len() as u32;
- if self.vertex_count == 0 {
- return;
- }
- let data = bytemuck::cast_slice(&verts);
- let needed = data.len() as wgpu::BufferAddress;
- if needed > self.vertex_buffer.size() {
- self.vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Vertex Buffer"),
- size: needed,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
+ fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
+ match msg {
+ DemoMessage::Exit => *exit = true,
}
- self.queue.write_buffer(&self.vertex_buffer, 0, data);
}
- fn update_status_text(&mut self, text: &str) {
- self.status_buffer = make_text_buffer(&mut self.font_system, text, 12.0);
- }
-
- fn prepare_text(&mut self) {
- let Self {
- ref mut text_renderer,
- ref device,
- ref queue,
- ref mut font_system,
- ref mut text_atlas,
- ref mut text_viewport,
- ref mut swash_cache,
- ref label_buffer,
- ref status_buffer,
- physical_width,
- physical_height,
- scale,
- ref json_layout,
- layout_mode,
- ref ui_context,
- ..
- } = self;
-
- let viewport = Resolution { width: *physical_width, height: *physical_height };
- text_viewport.update(queue, viewport);
-
- let scale_f32 = *scale as f32;
-
- let mut areas: Vec<TextArea> = Vec::new();
- if !*layout_mode {
- areas.push(TextArea {
- buffer: label_buffer,
- left: 80.0 * scale_f32,
- top: 12.0 * scale_f32,
- scale: scale_f32,
- bounds: TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- },
- default_color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
- custom_glyphs: &[],
- });
- areas.push(TextArea {
- buffer: status_buffer,
- left: 12.0 * scale_f32,
- top: *physical_height as f32 - 24.0 * scale_f32,
- scale: scale_f32,
- bounds: TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- },
- default_color: glyphon::Color::rgb(0x55, 0x55, 0x66),
- custom_glyphs: &[],
- });
+ /// Widget animations (cursor blink, hover fades) tick through the UiContext; the
+ /// loop is demand-driven, so returning a redraw request only when something moved
+ /// keeps the app idle otherwise.
+ fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
+ if self.ui_context.tick(dt) {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
}
+ }
- let mut widget_buffers: Vec<Buffer> = Vec::new();
- let mut widget_labels: Vec<(TextLabel, Option<[f32; 4]>)> = Vec::new();
- if *layout_mode {
- if let Some(jl) = json_layout {
- for (label, font, bounds) in jl.text_labels_with_font_and_bounds(ui_context) {
- widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
- widget_labels.push((label, bounds));
- }
- }
- } else {
- for (_i, w) in self.widgets.iter().enumerate() {
- for (label, font, bounds) in w.text_labels_with_font_and_bounds(ui_context) {
- widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
- widget_labels.push((label, bounds));
- }
- }
-
- if cce_ui::widget::context_menu::is_visible() {
- for label in cce_ui::widget::context_menu::text_labels() {
- widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
- widget_labels.push((label, None));
+ fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
+ // Register once: the registry backs the router (drag targets are looked up by
+ // widget id) and drag_allowed_at. Pointers into `self` are stable only once
+ // `self` sits at its final address — hence here, not in `new()`.
+ if !self.widgets_registered {
+ self.widgets_registered = true;
+ let self_ptr = self as *mut Self;
+ unsafe {
+ for w in (*self_ptr).roots() {
+ let id = (*w).base().unwrap().id();
+ self.ui_context.register_widget(id, w);
}
}
}
- for (buf, (label, bounds)) in widget_buffers.iter().zip(widget_labels.iter()) {
- let item_bounds = if let Some([l, t, r, b]) = bounds {
- TextBounds {
- left: (l * scale_f32).round() as i32,
- top: (t * scale_f32).round() as i32,
- right: (r * scale_f32).round() as i32,
- bottom: (b * scale_f32).round() as i32,
- }
- } else {
- TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- }
- };
- areas.push(TextArea {
- buffer: buf,
- left: label.x * scale_f32,
- top: label.y * scale_f32,
- scale: scale_f32,
- bounds: item_bounds,
- default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
- custom_glyphs: &[],
- });
- }
-
- let mut popover_pc = cce_ui::layout::PopoverCollector::new();
- let mut popover_buffers = Vec::new();
-
- if !*layout_mode {
- // Draw popover texts on top
- for w in &self.widgets {
- if w.popover_rect().is_some() {
- w.render_popover(&mut popover_pc);
+ let size_changed = self.width != size.width as u32
+ || self.height != size.height as u32
+ || self.scale_factor != scale;
+ if self.needs_rebuild || size_changed {
+ self.width = size.width as u32;
+ self.height = size.height as u32;
+ self.scale_factor = scale;
+ cce_ui::scale::set_scale_factor(scale as f32);
+
+ // Re-assert widget visuals from app state (the app is the source of truth).
+ self.toggle.set_toggled(self.toggle_on);
+ self.toggle.set_label(if self.toggle_on { "ON" } else { "OFF" });
+
+ // ── Layout: a plain LayoutBox tree, solved in one call. Leaves carry their
+ // intrinsic sizes; `grow` distributes leftover space; the solved rects are
+ // assigned straight onto the widgets.
+ let mut arena: Arena<LayoutBox> = Arena::new();
+ let root = arena.insert(LayoutBox::container(
+ Style::column().padding(16.0).gap(14.0).cross_align(CrossAlign::Stretch),
+ ));
+ let title = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 22.0)));
+ let controls = arena.insert(LayoutBox::container(
+ Style::row().gap(14.0).height(Length::Fixed(28.0)),
+ ));
+ // `shrink` lets the fixed leaves give up width when the window is at its
+ // minimum instead of overflowing the row.
+ let button = arena.insert(LayoutBox::leaf(Style::row().shrink(1.0), LSize::new(120.0, 28.0)));
+ let toggle = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(64.0, 28.0)));
+ let dropdown = arena.insert(LayoutBox::leaf(Style::row().shrink(1.0), LSize::new(150.0, 26.0)));
+ let slider = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 24.0)));
+ let name_box = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 30.0)));
+ let spacer = arena.insert(LayoutBox::container(Style::column().grow(1.0)));
+ let status = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 18.0)));
+ arena.append_child(root, title);
+ arena.append_child(root, controls);
+ arena.append_child(controls, button);
+ arena.append_child(controls, toggle);
+ arena.append_child(controls, dropdown);
+ arena.append_child(root, slider);
+ arena.append_child(root, name_box);
+ arena.append_child(root, spacer);
+ arena.append_child(root, status);
+ compute_layout(
+ &mut arena,
+ root,
+ LSize::new(self.width as f32, self.height as f32),
+ );
+
+ // Stretched children fill the column width; fixed leaves keep their size.
+ let r = |id| arena.value(id).unwrap().rect;
+ let b = r(button);
+ self.button.set_rect(b.x, b.y, b.width, b.height);
+ let t = r(toggle);
+ self.toggle.set_rect(t.x, t.y, t.width, t.height);
+ let d = r(dropdown);
+ self.theme_dropdown.set_rect(d.x, d.y, d.width, d.height);
+ let s = r(slider);
+ self.slider.set_rect(s.x, s.y, s.width, s.height);
+ let n = r(name_box);
+ self.name_box.set_rect(n.x, n.y, n.width, n.height);
+ self.title_rect = r(title);
+ self.status_rect = r(status);
+
+ self.needs_rebuild = false;
+ self.ui_context.rebuild_spatial_grid();
+ }
+
+ // ── Popover registration: ui_context ONLY. It drives the engine's display-list
+ // text occlusion clamp (labels under the open popover get clipped); the popover
+ // itself is drawn into this frame below — there is no popup surface.
+ self.ui_context.clear_popovers();
+ if self.theme_dropdown.popover_rect().is_some() {
+ self.ui_context.register_popover(&self.theme_dropdown);
+ }
+
+ let mut pc = PaintCtx::new();
+ let w = self.width as f32;
+ let h = self.height as f32;
+
+ // The window plate — the dissolved root Backplate's exact paint: page-low color
+ // at the configured opacity, config corner radius.
+ let mut plate = cce_ui::color::page_low_color();
+ if plate[3] > 0.001 {
+ plate[3] = cce_ui::color::active_backplate_opacity();
+ }
+ let radius = cce_ui::colors::backplate_corner_radius();
+ let frame = Rect { x: 0.0, y: 0.0, width: w, height: h };
+ if radius > 0.1 {
+ pc.rounded_rect(frame, radius, (true, true, true, true), plate);
+ } else {
+ pc.quad(frame, plate);
+ }
+
+ // App chrome text: plain prims. `text_with` carries an optional font family and
+ // optional bounds; unbounded text is clamped to the surface by the engine.
+ pc.text_with(
+ "cce-ui reference gallery".to_string(),
+ self.title_rect.x,
+ self.title_rect.y,
+ 15.0,
+ [0xdd, 0xdd, 0xe2],
+ Some("monospace".to_string()),
+ None,
+ );
+ pc.text_with(
+ self.status.clone(),
+ self.status_rect.x,
+ self.status_rect.y,
+ 12.0,
+ [0x9a, 0x9a, 0xa4],
+ None,
+ None,
+ );
+
+ // Widgets: each root walked through the single paint pass. The walk recurses,
+ // clips, and emits each widget's own geometry AND text (`Adapted::paint_self`
+ // serves per-widget fonts and bounds).
+ {
+ let self_ptr = self as *mut Self;
+ unsafe {
+ for root in (*self_ptr).roots() {
+ cce_ui::scene::painter::paint_root_into(&self.ui_context, root, &mut pc);
}
}
- for (t, size, _x, _y, _tc, _font_opt, _bounds) in &popover_pc.texts {
- popover_buffers.push(make_text_buffer(font_system, t, *size));
- }
- for (buf, (_, _size, x, y, tc, _font_opt, bounds)) in popover_buffers.iter().zip(popover_pc.texts.iter()) {
- let item_bounds = if let Some([l, t, r, b]) = bounds {
- TextBounds {
- left: (l * scale_f32).round() as i32,
- top: (t * scale_f32).round() as i32,
- right: (r * scale_f32).round() as i32,
- bottom: (b * scale_f32).round() as i32,
- }
- } else {
- TextBounds {
- left: 0,
- top: 0,
- right: *physical_width as i32,
- bottom: *physical_height as i32,
- }
- };
- areas.push(TextArea {
- buffer: buf,
- left: *x * scale_f32,
- top: *y * scale_f32,
- scale: scale_f32,
- bounds: item_bounds,
- default_color: glyphon::Color::rgb(
- (tc[0] * 255.0) as u8,
- (tc[1] * 255.0) as u8,
- (tc[2] * 255.0) as u8,
- ),
- custom_glyphs: &[],
- });
- }
- }
-
- text_renderer
- .prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache)
- .unwrap();
- }
-
- fn resize(&mut self, width: u32, height: u32) {
- if width > 0 && height > 0 {
- self.physical_width = width;
- self.physical_height = height;
- self.width = width as f32 / self.scale as f32;
- self.height = height as f32 / self.scale as f32;
- self.config.width = width;
- self.config.height = height;
- self.surface.configure(&self.device, &self.config);
- if self.layout_mode {
- self.positions = vec![(0.0, 0.0, self.width, self.height)];
- } else {
- self.positions = demo_positions(self.width, self.height);
- }
- self.apply_layout();
- self.upload_vertices();
}
- }
- fn render(&mut self) {
- self.prepare_text();
-
- let output = match self.surface.get_current_texture() {
- Ok(t) => t,
- Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
- self.surface.configure(&self.device, &self.config);
- match self.surface.get_current_texture() {
- Ok(t) => t,
- Err(e) => {
- log::error!("Surface error after configure: {e:?}");
- return;
- }
- }
+ // The dropdown popover — geometry and labels last, on top of everything, exactly
+ // where it hit-tests. Labels carry bounds equal to the popover rect: that clips
+ // them to the plate AND exempts them from the occlusion clamp (text whose bounds
+ // equal an overlay rect is treated as the overlay's own).
+ if let Some((px, py, pw, ph)) = self.theme_dropdown.popover_rect() {
+ let mut coll = cce_ui::layout::PopoverCollector::new();
+ self.theme_dropdown.render_popover(&mut coll);
+ for &(c, x, y, qw, qh) in &coll.rects {
+ pc.quad(Rect { x, y, width: qw, height: qh }, c);
}
- Err(wgpu::SurfaceError::Timeout) => return,
- Err(e) => {
- log::error!("Surface error: {e:?}");
- return;
+ let bounds = Some([px, py, px + pw, py + ph]);
+ for (content, size, tx, ty, color, font, _b) in coll.texts {
+ let color_u8 = [
+ (color[0] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[1] * 255.0).clamp(0.0, 255.0) as u8,
+ (color[2] * 255.0).clamp(0.0, 255.0) as u8,
+ ];
+ pc.text_with(content, tx, ty, size, color_u8, font, bounds);
}
- };
-
- let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
-
- let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
- label: Some("Encoder"),
- });
-
- {
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("Render Pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: &view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color {
- r: 0.05,
- g: 0.05,
- b: 0.08,
- a: 0.20,
- }),
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- if self.vertex_count > 0 {
- pass.set_pipeline(&self.render_pipeline);
- pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
- pass.draw(0..self.vertex_count, 0..1);
- }
-
- self.text_renderer
- .render(&self.text_atlas, &self.text_viewport, &mut pass)
- .unwrap();
}
- self.queue.submit(std::iter::once(encoder.finish()));
- output.present();
+ Some(pc.finish())
}
-}
-
-fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
- vec![
- (0.0, 0.0, sw, 40.0), // 0 header
- (0.0, 40.0, 60.0, sh - 68.0), // 1 sidebar
- (60.0, 40.0, sw - 60.0, sh - 68.0), // 2 content_bg
- (70.0, 50.0, 140.0, 40.0), // 3 btn_a
- (220.0, 50.0, 140.0, 40.0), // 4 btn_b
- (370.0, 50.0, 140.0, 40.0), // 5 btn_c
- (70.0, 100.0, 400.0, 250.0), // 6 panel
- (70.0, 360.0, 140.0, 40.0), // 7 click_me
- (220.0, 360.0, 140.0, 40.0), // 8 reset
- (70.0, 410.0, 24.0, 24.0), // 9 checkbox
- (104.0, 410.0, 48.0, 24.0), // 10 toggle
- (162.0, 410.0, 160.0, 24.0), // 11 progress_bar
- (70.0, 444.0, 300.0, 32.0), // 12 slider
- (70.0, 486.0, 120.0, 32.0), // 13 spinbox
- (70.0, 528.0, 300.0, 32.0), // 14 range_slider
- (0.0, sh - 28.0, sw, 28.0), // 15 status_bar
- ]
-}
-struct PressedKey {
- logical_key: cce_ui::widget::Key,
- text: Option<String>,
- first_pressed: std::time::Instant,
- last_repeated: std::time::Instant,
-}
-
-fn is_repeatable_key(key: &cce_ui::widget::Key) -> bool {
- use cce_ui::widget::{Key, NamedKey};
- match key {
- Key::Named(NamedKey::Backspace) |
- Key::Named(NamedKey::Delete) |
- Key::Named(NamedKey::ArrowLeft) |
- Key::Named(NamedKey::ArrowRight) |
- Key::Named(NamedKey::ArrowUp) |
- Key::Named(NamedKey::ArrowDown) |
- Key::Named(NamedKey::Home) |
- Key::Named(NamedKey::End) |
- Key::Character(_) => true,
- _ => false,
+ /// Text prims in the display list ARE the frame's text — no `text_items` twin.
+ fn display_list_text(&self) -> bool {
+ true
}
-}
-
-struct AppState {
- registry_state: RegistryState,
- compositor_state: CompositorState,
- xdg_shell_state: XdgShell,
- shm_state: Shm,
- seat_state: SeatState,
- output_state: OutputState,
-
- seats: Vec<wl_seat::WlSeat>,
- pointer: Option<ThemedPointer>,
- keyboard: Option<wl_keyboard::WlKeyboard>,
- window: Option<XdgWindow>,
- surface: Option<wl_surface::WlSurface>,
-
- state: Option<State>,
- exit: bool,
- redraw: bool,
- ctrl_pressed: bool,
- shift_pressed: bool,
- pressed_key: Option<PressedKey>,
- key_repeat_delay: std::time::Duration,
- key_repeat_interval: std::time::Duration,
- inspector: Option<cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1>,
- last_inspector_update: std::time::Instant,
- last_serialized: String,
-}
-
-impl CompositorHandler for AppState {
- fn scale_factor_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- scale_factor: i32,
- ) {
- _surface.set_buffer_scale(scale_factor);
- if let Some(state) = &mut self.state {
- state.scale = scale_factor as f64;
- let pw = (state.width as f64 * state.scale) as u32;
- let ph = (state.height as f64 * state.scale) as u32;
- state.resize(pw, ph);
- }
- self.redraw = true;
- }
-
- fn transform_changed(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _new_transform: wl_output::Transform,
- ) {
- }
-
- fn frame(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _time: u32,
- ) {
+ fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
+ Some(&self.ui_context)
}
- fn surface_enter(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {
- }
-
- fn surface_leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _surface: &wl_surface::WlSurface,
- _output: &wl_output::WlOutput,
- ) {
+ /// Window dragging for a dissolved root: the surface is the movable plate; drag
+ /// anywhere a drag-blocking registered widget isn't.
+ fn is_movable_backplate_at(&self, px: f32, py: f32) -> bool {
+ self.ui_context.drag_allowed_at(px, py)
}
-}
-impl OutputHandler for AppState {
- fn output_state(&mut self) -> &mut OutputState {
- &mut self.output_state
+ fn clear_color(&self) -> [f32; 4] {
+ [0.0, 0.0, 0.0, 0.0]
}
- fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-
- fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-
- fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
-}
-
-impl SeatHandler for AppState {
- fn seat_state(&mut self) -> &mut SeatState {
- &mut self.seat_state
- }
-
- fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.push(seat);
- }
-
- fn new_capability(
- &mut self,
- _conn: &Connection,
- qh: &QueueHandle<Self>,
- seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer && self.pointer.is_none() {
- let surface = self.compositor_state.create_surface(qh);
- let themed_pointer = self.seat_state.get_pointer_with_theme(
- qh,
- &seat,
- self.shm_state.wl_shm(),
- surface,
- ThemeSpec::System,
- ).unwrap();
- self.pointer = Some(themed_pointer);
- }
- if capability == Capability::Keyboard && self.keyboard.is_none() {
- let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
- self.keyboard = Some(keyboard);
- }
- }
-
- fn remove_capability(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _seat: wl_seat::WlSeat,
- capability: Capability,
- ) {
- if capability == Capability::Pointer {
- self.pointer = None;
+ fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+ let (px, py) = (pos.x, pos.y);
+ let ev = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
+ let mut changed = false;
+ let self_ptr = self as *mut Self;
+ unsafe {
+ // PointerMove visits every root: hover bookkeeping everywhere, and the
+ // router forwards DragUpdate to the recorded drag target (slider thumb,
+ // text selection) once its 3px threshold trips.
+ for root in (*self_ptr).roots() {
+ if self.ui_context.propagate_event(&ev, root) {
+ changed = true;
+ }
+ }
}
- if capability == Capability::Keyboard {
- self.keyboard = None;
+ self.drain_widget_changes();
+ if changed || self.needs_rebuild {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
}
}
- fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
- self.seats.retain(|s| s != &seat);
- }
-}
-
-impl ShmHandler for AppState {
- fn shm_state(&mut self) -> &mut Shm {
- &mut self.shm_state
- }
-}
-
-impl PointerHandler for AppState {
- fn pointer_frame(
+ fn handle_mouse_input(
&mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _pointer: &wl_pointer::WlPointer,
- events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
- ) {
- use smithay_client_toolkit::seat::pointer::PointerEventKind;
- let mut coalesced_h = 0.0f64;
- let mut coalesced_v = 0.0f64;
- let mut discrete_h = 0;
- let mut discrete_v = 0;
- let mut has_scroll = false;
- for event in events {
- let (x, y) = event.position;
- if let Some(state) = &mut self.state {
- state.cursor_x = x as f32;
- state.cursor_y = y as f32;
- }
-
- match &event.kind {
- PointerEventKind::Enter { .. } => {
- if let Some(ref themed_pointer) = self.pointer {
- let _ = themed_pointer.set_cursor(_conn, CursorIcon::Default);
- }
- }
- PointerEventKind::Leave { .. } => {}
- PointerEventKind::Motion { .. } => {
- if let Some(state) = &mut self.state {
- let mut changed = false;
- if cce_ui::widget::context_menu::is_visible() {
- if cce_ui::widget::context_menu::cursor_moved(state.cursor_x, state.cursor_y) {
- changed = true;
- }
- } else if state.layout_mode {
- if let Some(jl) = &mut state.json_layout {
- if jl.cursor_moved(state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
- } else {
- if let Some(idx) = state.drag_widget {
- if state.widgets[idx].drag_update(state.cursor_x, state.cursor_y) {
- changed = true;
- }
- }
- if state.drag_widget.is_none() {
- for w in &mut state.widgets {
- if w.cursor_moved(state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
- }
- }
- if changed {
- state.upload_vertices();
- self.redraw = true;
- }
- }
- }
- PointerEventKind::Press { button, .. } => {
- let btn = match *button {
- 272 => cce_ui::widget::MouseButton::Left,
- 273 => cce_ui::widget::MouseButton::Right,
- 274 => cce_ui::widget::MouseButton::Middle,
- _ => continue,
- };
- if let Some(st) = &mut self.state {
- let mut changed = false;
- if cce_ui::widget::context_menu::is_visible() {
- if cce_ui::widget::context_menu::mouse_input(btn, cce_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y) {
- changed = true;
- }
- } else if st.layout_mode {
- if let Some(jl) = &mut st.json_layout {
- if jl.mouse_input(btn, cce_ui::widget::ElementState::Pressed, st.cursor_x, st.cursor_y, &mut st.ui_context) {
- changed = true;
- }
- }
- } else {
- let mut clicked_idx = None;
- for i in (0..st.widgets.len()).rev() {
- if st.widgets[i].hit_test(st.cursor_x, st.cursor_y, &st.ui_context) {
- clicked_idx = Some(i);
- break;
- }
- }
- if btn == cce_ui::widget::MouseButton::Left {
- if let Some(old) = st.focused_widget {
- if Some(old) != clicked_idx {
- st.widgets[old].unfocus();
- st.focused_widget = None;
- }
- }
- }
- if let Some(i) = clicked_idx {
- if st.widgets[i].mouse_input(
- btn,
- cce_ui::widget::ElementState::Pressed,
- st.cursor_x,
- st.cursor_y,
- &mut st.ui_context,
- ) {
- changed = true;
- }
- if btn == cce_ui::widget::MouseButton::Left && st.widgets[i].draggable() {
- st.widgets[i].drag_begin(st.cursor_x, st.cursor_y);
- st.drag_widget = Some(i);
- }
- if btn == cce_ui::widget::MouseButton::Left {
- st.widgets[i].focus();
- st.focused_widget = Some(i);
- }
- }
- }
- if changed {
- st.upload_vertices();
- self.redraw = true;
- }
- }
- }
- PointerEventKind::Release { button, .. } => {
- let btn = match *button {
- 272 => cce_ui::widget::MouseButton::Left,
- 273 => cce_ui::widget::MouseButton::Right,
- 274 => cce_ui::widget::MouseButton::Middle,
- _ => continue,
- };
- if let Some(st) = &mut self.state {
- let mut changed = false;
- let mut should_close = false;
- if cce_ui::widget::context_menu::is_visible() {
- if cce_ui::widget::context_menu::mouse_input(btn, cce_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y) {
- changed = true;
- }
- } else if st.layout_mode {
- let mut clicked_btn_id = None;
- if let Some(jl) = &mut st.json_layout {
- if jl.mouse_input(btn, cce_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y, &mut st.ui_context) {
- changed = true;
- }
- if btn == cce_ui::widget::MouseButton::Left {
- for w in &mut jl.widgets {
- let active_page = 0;
- if w.page_idx != active_page {
- continue;
- }
- // take_click is an Element method; Phase 5 Buttons
- // are Adapted, so ask the box directly.
- if w.widget_type == "button" && w.widget.take_click() {
- clicked_btn_id = Some(w.id.clone());
- break;
- }
- }
- }
- }
- if let Some(btn_id) = clicked_btn_id {
- let mut checkboxes = std::collections::HashMap::new();
- let mut spinboxes = std::collections::HashMap::new();
- let mut colors = std::collections::HashMap::new();
- let mut sliders = std::collections::HashMap::new();
- if let Some(jl) = &st.json_layout {
- for w in &jl.widgets {
- if let Some(cb) = w.widget.as_any().downcast_ref::<cce_ui::widget::Checkbox>() {
- checkboxes.insert(w.id.clone(), cb.checked());
- } else if let Some(sb) = w.widget.as_any().downcast_ref::<cce_ui::widget::Spinbox>() {
- spinboxes.insert(w.id.clone(), sb.value);
- } else if let Some(cs) = w.widget.as_any().downcast_ref::<cce_ui::widget::ColorSelector>() {
- colors.insert(w.id.clone(), cs.color);
- } else if let Some(sl) = w.widget.as_any().downcast_ref::<cce_ui::widget::Slider>() {
- sliders.insert(w.id.clone(), sl.get_scaled_value());
- }
- }
- }
- let out_val = serde_json::json!({
- "button": btn_id,
- "checkboxes": checkboxes,
- "spinboxes": spinboxes,
- "colors": colors,
- "sliders": sliders
- });
- println!("{}", out_val.to_string());
- should_close = true;
- }
- } else {
- if btn == cce_ui::widget::MouseButton::Left {
- if let Some(idx) = st.drag_widget {
- st.widgets[idx].drag_end();
- st.drag_widget = None;
- changed = true;
- }
- }
- for w in &mut st.widgets {
- if w.mouse_input(btn, cce_ui::widget::ElementState::Released, st.cursor_x, st.cursor_y, &mut st.ui_context) {
- changed = true;
- }
- }
- if btn == cce_ui::widget::MouseButton::Left {
- let mut clicked = false;
- for w in &mut st.widgets {
- if w.take_click() {
- clicked = true;
- }
- }
- if clicked {
- st.click_count += 1;
- st.update_status_text(&format!("Clicks: {}", st.click_count));
- }
- }
- }
- if changed {
- st.upload_vertices();
- self.redraw = true;
- }
- if should_close {
- self.exit = true;
- }
- }
- }
- PointerEventKind::Axis { horizontal, vertical, .. } => {
- coalesced_h += horizontal.absolute;
- coalesced_v += vertical.absolute;
- discrete_h += horizontal.discrete;
- discrete_v += vertical.discrete;
- has_scroll = true;
+ button: MouseButton,
+ state: ElementState,
+ pos: LogicalPosition,
+ needs_rebuild: &mut bool,
+ ) -> Option<Self::Message> {
+ let (px, py) = (pos.x, pos.y);
+ let ev = Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
+ let mut changed = false;
+ let self_ptr = self as *mut Self;
+ unsafe {
+ // Presses are hit-gated per widget by the adapter and releases delivered
+ // everywhere (press-tracking widgets commit or cancel on them) — a straight
+ // loop is correct for pointer-positioned events.
+ for root in (*self_ptr).roots() {
+ if self.ui_context.propagate_event(&ev, root) {
+ changed = true;
}
}
}
-
- if has_scroll {
- if let Some(state) = &mut self.state {
- let delta = if discrete_h == 0 && discrete_v == 0 {
- cce_ui::widget::MouseScrollDelta::PixelDelta(cce_ui::widget::Position {
- x: -coalesced_h,
- y: -coalesced_v,
- })
- } else {
- let h_lines = if discrete_h != 0 { discrete_h as f32 } else { coalesced_h as f32 / 10.0 };
- let v_lines = if discrete_v != 0 { discrete_v as f32 } else { coalesced_v as f32 / 10.0 };
- cce_ui::widget::MouseScrollDelta::LineDelta(-h_lines, -v_lines)
- };
-
- let mut changed = false;
- if !state.layout_mode {
- for w in &mut state.widgets {
- if w.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
- } else {
- if let Some(jl) = &mut state.json_layout {
- if jl.mouse_wheel(&delta, state.cursor_x, state.cursor_y, &mut state.ui_context) {
- changed = true;
- }
- }
- }
- if changed {
- state.upload_vertices();
- self.redraw = true;
- }
- }
+ self.drain_widget_changes();
+ if changed || self.needs_rebuild {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
}
+ None
}
-}
-impl KeyboardHandler for AppState {
- fn enter(
+ fn handle_mouse_wheel(
&mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- _raw_modifiers: &[u32],
- _keysyms: &[xkeysym::Keysym],
+ delta: &MouseScrollDelta,
+ pos: LogicalPosition,
+ needs_rebuild: &mut bool,
) {
- }
-
- fn leave(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _surface: &wl_surface::WlSurface,
- _serial: u32,
- ) {
- self.pressed_key = None;
- self.ctrl_pressed = false;
- self.shift_pressed = false;
- }
-
- fn press_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {
- self.handle_key(event, cce_ui::widget::ElementState::Pressed);
- }
-
- fn release_key(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- event: smithay_client_toolkit::seat::keyboard::KeyEvent,
- ) {
- self.handle_key(event, cce_ui::widget::ElementState::Released);
- }
-
- fn update_modifiers(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- _serial: u32,
- modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
- _layout: u32,
- ) {
- self.ctrl_pressed = modifiers.ctrl;
- self.shift_pressed = modifiers.shift;
- }
-
- fn update_repeat_info(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _keyboard: &wl_keyboard::WlKeyboard,
- info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
- ) {
- match info {
- smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
- self.key_repeat_delay = std::time::Duration::from_millis(delay as u64);
- let interval_ms = 1000 / rate.get() as u64;
- self.key_repeat_interval = std::time::Duration::from_millis(interval_ms);
- }
- smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {
- self.key_repeat_delay = std::time::Duration::from_secs(999999);
- }
- }
- }
-}
-
-impl AppState {
- fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: cce_ui::widget::ElementState) {
- use cce_ui::widget::{Key, KeyEvent, NamedKey};
- let logical_key = match event.keysym {
- xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
- xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
- xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
- xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
- xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
- xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
- xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
- xkeysym::Keysym::Tab => Key::Named(NamedKey::Tab),
- xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
- xkeysym::Keysym::space => Key::Named(NamedKey::Space),
- _ => {
- if let Some(ref text) = event.utf8 {
- Key::Character(text.clone())
- } else if let Some(ch) = event.keysym.key_char() {
- Key::Character(ch.to_string())
- } else {
- return;
- }
- }
- };
-
- let custom_event = KeyEvent {
- state,
- logical_key,
- text: event.utf8.clone(),
- repeat: false,
- ctrl: self.ctrl_pressed,
- shift: self.shift_pressed,
- };
-
- if cce_ui::widget::context_menu::is_visible() {
- if custom_event.state == cce_ui::widget::ElementState::Pressed
- && custom_event.logical_key == Key::Named(NamedKey::Escape)
- {
- cce_ui::widget::context_menu::hide();
- if let Some(st) = &mut self.state {
- st.upload_vertices();
- }
- self.redraw = true;
- return;
- }
- }
-
- if state == cce_ui::widget::ElementState::Pressed {
- if is_repeatable_key(&custom_event.logical_key) {
- self.pressed_key = Some(PressedKey {
- logical_key: custom_event.logical_key.clone(),
- text: custom_event.text.clone(),
- first_pressed: std::time::Instant::now(),
- last_repeated: std::time::Instant::now(),
- });
- } else {
- self.pressed_key = None;
- }
- } else if state == cce_ui::widget::ElementState::Released {
- if let Some(ref pk) = self.pressed_key {
- if pk.logical_key == custom_event.logical_key {
- self.pressed_key = None;
- }
- }
- }
-
- if let Some(st) = &mut self.state {
- if st.layout_mode {
- if let Some(jl) = &mut st.json_layout {
- let changed = jl.keyboard_input(&custom_event, &mut st.ui_context);
- if changed {
- st.upload_vertices();
- self.redraw = true;
- }
- }
- } else if let Some(idx) = st.focused_widget {
- let val = st.widgets[idx].value();
- let mut changed = st.widgets[idx].keyboard_input(&custom_event, &mut st.ui_context);
- if st.widgets[idx].value() != val {
+ let (px, py) = (pos.x, pos.y);
+ let ev = Event::MouseWheel { delta: delta.clone(), x: px, y: py, local_x: px, local_y: py };
+ let mut changed = false;
+ let self_ptr = self as *mut Self;
+ unsafe {
+ // Wheel is hit-scoped per widget (the slider nudges its value under the
+ // cursor); roots that miss return false.
+ for root in (*self_ptr).roots() {
+ if self.ui_context.propagate_event(&ev, root) {
changed = true;
}
- if changed {
- st.upload_vertices();
- self.redraw = true;
- }
}
}
- }
-}
-
-impl WindowHandler for AppState {
- fn configure(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _window: &XdgWindow,
- configure: WindowConfigure,
- _serial: u32,
- ) {
- let (w, h) = configure.new_size;
- if let (Some(w), Some(h)) = (w, h) {
- let width = w.get();
- let height = h.get();
- if let Some(state) = &mut self.state {
- let pw = (width as f64 * state.scale) as u32;
- let ph = (height as f64 * state.scale) as u32;
- state.resize(pw, ph);
- }
+ self.drain_widget_changes();
+ if changed || self.needs_rebuild {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
}
- self.redraw = true;
}
- fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
- self.exit = true;
- }
-}
-
-impl ProvidesRegistryState for AppState {
- fn registry(&mut self) -> &mut RegistryState {
- &mut self.registry_state
- }
-
- fn runtime_add_global(
- &mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- _version: u32,
- ) {}
-
- fn runtime_remove_global(
+ fn handle_key_input(
&mut self,
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- _name: u32,
- _interface: &str,
- ) {}
-}
-
-impl wayland_client::Dispatch<cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1, ()> for AppState {
- fn event(
- _state: &mut Self,
- _proxy: &cce_ui::protocol::zcce_inspector_v1::ZcceInspectorV1,
- _event: cce_ui::protocol::zcce_inspector_v1::Event,
- _data: &(),
- _conn: &Connection,
- _qh: &QueueHandle<Self>,
- ) {}
-}
-
-delegate_compositor!(AppState);
-delegate_xdg_shell!(AppState);
-delegate_xdg_window!(AppState);
-delegate_shm!(AppState);
-delegate_seat!(AppState);
-delegate_pointer!(AppState);
-delegate_keyboard!(AppState);
-delegate_registry!(AppState);
-delegate_output!(AppState);
-
-fn main() {
- let mut layout_mode = false;
- let mut json_layout_config: Option<JsonLayoutConfig> = None;
-
- let args = std::env::args().skip(1).collect::<Vec<String>>();
- let mut idx = 0;
- while idx < args.len() {
- let arg = &args[idx];
- if arg == "--layout" || arg == "--json" {
- layout_mode = true;
- idx += 1;
- } else {
- idx += 1;
- }
- }
-
- if layout_mode {
- use std::io::Read;
- let mut json_str = String::new();
- let mut stdin = std::io::stdin();
- match stdin.read_to_string(&mut json_str) {
- Ok(_) => {
- match serde_json::from_str::<JsonLayoutConfig>(&json_str) {
- Ok(cfg) => {
- json_layout_config = Some(cfg);
- }
- Err(e) => {
- log::error!("Failed to parse JSON layout: {}", e);
- std::process::exit(1);
- }
+ event: &KeyEvent,
+ needs_rebuild: &mut bool,
+ ) -> Option<Self::Message> {
+ // App-level shortcuts before widget routing.
+ if event.ctrl && event.state == ElementState::Pressed {
+ if let cce_ui::widget::Key::Character(ref c) = event.logical_key {
+ if c == "q" {
+ return Some(DemoMessage::Exit);
}
}
- Err(e) => {
- log::error!("Failed to read JSON layout from stdin: {}", e);
- std::process::exit(1);
- }
- }
- }
-
- let conn = Connection::connect_to_env().unwrap();
- let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
- let qh = event_queue.handle();
-
- let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
- let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
- let shm_state = Shm::bind(&globals, &qh).unwrap();
- let seat_state = SeatState::new(&globals, &qh);
- let output_state = OutputState::new(&globals, &qh);
- let inspector = globals.bind(&qh, 1..=1, ()).ok();
-
- let mut app = AppState {
- registry_state: RegistryState::new(&globals),
- compositor_state,
- xdg_shell_state,
- shm_state,
- seat_state,
- output_state,
- seats: Vec::new(),
- pointer: None,
- keyboard: None,
- window: None,
- surface: None,
- state: None,
- exit: false,
- redraw: true,
- ctrl_pressed: false,
- shift_pressed: false,
- pressed_key: None,
- key_repeat_delay: std::time::Duration::from_millis(500),
- key_repeat_interval: std::time::Duration::from_millis(50),
- inspector,
- last_inspector_update: std::time::Instant::now() - std::time::Duration::from_secs(1),
- last_serialized: String::new(),
- };
-
- // Perform a roundtrip to populate output_state with active output scales
- event_queue.roundtrip(&mut app).unwrap();
-
- let scale = cce_ui::wayland::detect_scale_factor(&app.output_state);
-
- let surface = app.compositor_state.create_surface(&qh);
- surface.set_buffer_scale(scale as i32);
-
- let mut win_w = 1024.0;
- let mut win_h = 768.0;
- if let Some(ref cfg) = json_layout_config {
- if let Some(w) = cfg.width {
- win_w = w as f64;
- }
- if let Some(h) = cfg.height {
- win_h = h as f64;
- }
- }
- let pw = (win_w * scale) as u32;
- let ph = (win_h * scale) as u32;
-
- let window = app.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
- window.set_title("CCE UI - Test Window");
- window.set_app_id("cce-ui");
- window.set_min_size(Some((win_w as u32, win_h as u32)));
- window.commit();
-
- if let Some(ref inspector) = app.inspector {
- inspector.register_client(&surface);
- }
-
- let wayland_handle = Box::leak(Box::new(cce_ui::wayland::WaylandSurfaceHandle {
- display_ptr: conn.backend().display_id().as_ptr() as *mut std::ffi::c_void,
- surface_ptr: surface.id().as_ptr() as *mut std::ffi::c_void,
- }));
-
- let state = pollster::block_on(State::new(wayland_handle, pw, ph, scale, json_layout_config));
-
- app.window = Some(window);
- app.surface = Some(surface);
- app.state = Some(state);
-
- let mut event_loop = EventLoop::try_new().unwrap();
- let loop_handle = event_loop.handle();
- WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
-
-
-
- let mut last_tick = std::time::Instant::now();
-
- loop {
- event_loop
- .dispatch(std::time::Duration::from_millis(16), &mut app)
- .unwrap();
- if app.exit {
- break;
- }
-
- let now = std::time::Instant::now();
- let dt = now.duration_since(last_tick).as_secs_f32();
- last_tick = now;
-
- if let Some(ref mut st) = app.state {
- let tick_changed = st.ui_context.tick(dt);
- if tick_changed {
- st.upload_vertices();
- app.redraw = true;
- }
}
- if let Some(ref mut pk) = app.pressed_key {
- let now = std::time::Instant::now();
- if now.duration_since(pk.first_pressed) >= app.key_repeat_delay {
- if now.duration_since(pk.last_repeated) >= app.key_repeat_interval {
- pk.last_repeated = now;
- let custom_event = cce_ui::widget::KeyEvent {
- state: cce_ui::widget::ElementState::Pressed,
- logical_key: pk.logical_key.clone(),
- text: pk.text.clone(),
- repeat: true,
- ctrl: app.ctrl_pressed,
- shift: app.shift_pressed,
- };
- if let Some(st) = &mut app.state {
- if st.layout_mode {
- if let Some(jl) = &mut st.json_layout {
- let changed = jl.keyboard_input(&custom_event, &mut st.ui_context);
- if changed {
- st.upload_vertices();
- app.redraw = true;
- }
- }
- } else if let Some(idx) = st.focused_widget {
- let val = st.widgets[idx].value();
- let mut changed = st.widgets[idx].keyboard_input(&custom_event, &mut st.ui_context);
- if st.widgets[idx].value() != val {
- changed = true;
- }
- if changed {
- st.upload_vertices();
- app.redraw = true;
- }
- }
- }
+ // KeyInput MUST short-circuit: the router delivers keys to the ctx-focused
+ // widget FIRST on every propagate call, so a non-short-circuited chain would
+ // hand a typed character to the focused widget once per root (N-time
+ // insertion). Plumbing that would key off "which call handled it" belongs in
+ // the state-gated `drain_widget_changes` instead.
+ let ev = Event::KeyInput(event.clone());
+ let mut handled = false;
+ let self_ptr = self as *mut Self;
+ unsafe {
+ for root in (*self_ptr).roots() {
+ if self.ui_context.propagate_event(&ev, root) {
+ handled = true;
+ break;
}
}
}
-
-fn create_memfd_with_data(name: &str, data: &[u8]) -> std::io::Result<std::os::unix::io::RawFd> {
- use std::io::{Seek, Write};
- use std::os::unix::io::FromRawFd;
- use std::os::unix::io::IntoRawFd;
-
- let c_name = std::ffi::CString::new(name).unwrap();
- let fd = unsafe { libc::memfd_create(c_name.as_ptr(), libc::MFD_CLOEXEC) };
- if fd < 0 {
- return Err(std::io::Error::last_os_error());
+ self.drain_widget_changes();
+ if handled || self.needs_rebuild {
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ }
+ None
}
- let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
- file.write_all(data)?;
- file.seek(std::io::SeekFrom::Start(0))?;
- Ok(file.into_raw_fd())
}
- if app.redraw {
- app.redraw = false;
- if let Some(state) = &mut app.state {
- state.render();
- if let Some(ref inspector) = app.inspector {
- if let Some(ref surface) = app.surface {
- let json = cce_ui::widget::serialize_widgets(&state.widgets);
- if json != app.last_serialized {
- let now = std::time::Instant::now();
- if now.duration_since(app.last_inspector_update) >= std::time::Duration::from_millis(100) {
- app.last_serialized = json.clone();
- app.last_inspector_update = now;
- if let Ok(raw_fd) = create_memfd_with_data("cce_ui_state", json.as_bytes()) {
- use std::os::unix::io::{FromRawFd, AsFd};
- let file = unsafe { std::fs::File::from_raw_fd(raw_fd) };
- inspector.update_state(surface, file.as_fd(), json.len() as u32);
- }
- } else {
- app.redraw = true;
- }
- }
- }
- }
- }
- }
- }
+fn main() {
+ cce_ui::engine::run::<DemoApp>();
}