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

commit279ffc9bc0dd877c512cc102ea883d3a1d813bef
parentdd587180a1
authorLucas Galante <[email protected]>
date2026-07-10 09:40
feat(engine)!: delete the render-only xdg popup path (Phase 6x)

Every app draws its popovers and the global context menu into its own display
list now (text-editor and data-editor were the last consumers — migrated in
their repos), so the engine sheds: ActivePopup (per-popover wgpu surface,
viewport, vertex buffer), the xdg positioner/spawn/despawn block, the popup
render pass, popup-surface pointer-coordinate translation, PopupHandler +
delegate_xdg_popup, and the Application::render_popovers +
draws_own_popovers hooks. The context-menu dl-text occlusion rect is now
unconditional. Popovers render in-frame where they hit-test — the positioner
anchor-mismatch class of bugs (6s) is unrepresentable. The global
widget::popovers registry is write-only now; it goes with the legacy deletion.

Co-Authored-By: Claude Fable 5 <[email protected]>

 docs/rfc-core-rebuild.md     |  26 +++-
 src/backend/mod.rs           |   2 +-
 src/backend/window_runner.rs | 345 ++-----------------------------------------
 src/engine.rs                |   2 +-
 4 files changed, 39 insertions(+), 336 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 5175acb..415d804 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -1092,9 +1092,31 @@ Constraint respected: **each crate still builds standalone** — the new core is
     focus pointer kept aiming at it — it survived only because same-size Vec
     reallocation tends to reuse the freed block. Ctrl-nav is not headlessly
     drivable (no virtual-keyboard protocol) — user spot-check pending.
+  - **6x — data-editor + text-editor off the engine popup path; the render-only xdg
+    popup machinery is DELETED. DONE (live-verified: text-editor File menu open +
+    item click; data-editor recent-files menu → config.kdl load, tree context menu
+    with occlusion + Copy Key through the clipboard; settings page dropdown +
+    page switch unaffected after losing its gate).** Both apps now collect their
+    ui_context-registered popovers via `PopoverCollector` and emit them last in
+    the display list (data-editor appends the global context menu too), labels
+    bounded to the overlay rect — the 6l/6t recipe; registration is
+    ui_context-only. With the last consumers across, the engine sheds the whole
+    popup path: `ActivePopup` (wgpu surface + viewport + vertex buffer per
+    popover), the xdg positioner/spawn/despawn block in the event loop, the popup
+    render pass, the popup-surface pointer-coordinate translation, the
+    `PopupHandler` + `delegate_xdg_popup` plumbing, and the
+    `Application::render_popovers` + `draws_own_popovers` hooks (settings'
+    override removed; the context-menu dl-text occlusion rect is now
+    unconditional). Every popover in the workspace is app-drawn, in-frame, where
+    it hit-tests — the 6s below-window-popover class of positioner bugs is
+    unrepresentable. NOTE: the compositor-side dismissal in `PopupHandler::done`
+    (unfocus popovers + hide context menu when the popup was dismissed) went with
+    it — in-frame apps already own dismissal (press-outside), same as
+    fonts/settings. The global `widget::popovers` registry is now write-only
+    (apps still clear/register into it) — delete it with the legacy paths.
+    Drive-by: cce-designer had not compiled since 6k (direct `WgpuAdapter::new`
+    call missing the new `load_system_fonts` bool) — fixed.
   - **Still to do:**
-    data-editor +
-    text-editor off the engine popup path, then delete the popup surface machinery;
     files' internal containers (splitters/BrowseContainer/List) and data-editor's
     SplitBox/TreeList when their turns come; routed events + scene layout for the
     widget-tree apps; the demo (`cce-ui/src/main.rs`) as the reference
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 4652a0c..a796238 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -4,5 +4,5 @@ pub mod window_runner;
 pub use wgpu_adapter::WgpuAdapter;
 pub use window_runner::{
     EngineState, WindowSettings, LogicalPosition, LogicalSize, Application, run,
-    Vertex, LineCap, ActivePopup, PressedKey, get_text_buffer,
+    Vertex, LineCap, PressedKey, get_text_buffer,
 };
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 595c2fa..f5f3e78 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2,7 +2,7 @@ use std::time::Instant;
 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, delegate_xdg_popup,
+    delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
     delegate_layer,
     registry::{ProvidesRegistryState, RegistryState},
     output::{OutputHandler, OutputState},
@@ -31,33 +31,16 @@ use wayland_protocols::wp::pointer_gestures::zv1::client::{
     zwp_pointer_gesture_pinch_v1::{self, ZwpPointerGesturePinchV1},
     zwp_pointer_gestures_v1::{self as zwp_pointer_gestures, ZwpPointerGesturesV1},
 };
-use smithay_client_toolkit::shell::xdg::popup::{Popup, PopupHandler, PopupConfigure};
-use smithay_client_toolkit::shell::xdg::{XdgPositioner, XdgSurface};
-use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity, ConstraintAdjustment};
-
 use calloop::EventLoop;
 use calloop_wayland_source::WaylandSource;
 use glyphon::{
-    Cache, FontSystem, Resolution, TextArea,
-    TextBounds, Viewport, Buffer, Attrs, Metrics,
+    FontSystem, Resolution, TextArea,
+    TextBounds, Buffer, Attrs, Metrics,
 };
 use crate::widget::{Element, TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey, Position};
-use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
+use crate::wayland::detect_scale_factor;
 use crate::backend::WgpuAdapter;
 
-pub struct ActivePopup {
-    pub sctk_popup: Popup,
-    pub wgpu_surface: wgpu::Surface<'static>,
-    pub config: wgpu::SurfaceConfiguration,
-    pub logical_width: f32,
-    pub logical_height: f32,
-    pub configured: bool,
-    pub viewport: Viewport,
-    pub x: f32,
-    pub y: f32,
-    pub vertex_buffer: Option<wgpu::Buffer>,
-}
-
 #[derive(Hash, PartialEq, Eq, Clone)]
 struct BufferCacheKey {
     text: String,
@@ -242,10 +225,6 @@ pub fn get_text_buffer_attrs(
     buf
 }
 
-fn make_text_buffer_with_font(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
-    get_text_buffer(fs, text, size, font)
-}
-
 /// The popover-occlusion clamp shared by the default [`Application::text_areas`] mapping and
 /// the display-list text path: clip a text item's bounds so it does not bleed through an open
 /// popover's plate. A text item whose own bounds coincide with a popover rect IS that popover's
@@ -1547,7 +1526,6 @@ pub trait Application: Sized + 'static {
     fn text_items(&self) -> &[TextItem] {
         &[]
     }
-    fn render_popovers(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
     fn input_regions(&self) -> Option<Vec<(i32, i32, i32, i32)>> {
         None
     }
@@ -1667,15 +1645,6 @@ pub trait Application: Sized + 'static {
     fn load_system_fonts(&self) -> bool {
         false
     }
-
-    /// Phase 6 opt-in: this app draws its popovers and context menu INTO its display list
-    /// (registered in `ui_context` for the occlusion clamp) — the engine must NOT spawn its
-    /// render-only xdg popup for globally-registered popovers or the global context menu.
-    /// Default `false`: unmigrated apps keep the popup surface + `render_popovers` collector
-    /// path. The popup path (and this flag) go away once its last consumer is across.
-    fn draws_own_popovers(&self) -> bool {
-        false
-    }
 }
 
 pub struct PressedKey {
@@ -1739,7 +1708,6 @@ pub struct EngineState<A: Application> {
     pub logo_pressed: bool,
     pub pressed_key: Option<PressedKey>,
     pub sender: calloop::channel::Sender<A::Message>,
-    pub active_popup: Option<ActivePopup>,
     pub current_cursor_icon: Option<CursorIcon>,
     pub qh: QueueHandle<EngineState<A>>,
     pub just_configured: bool,
@@ -2008,10 +1976,11 @@ impl<A: Application> EngineState<A> {
                 }
             }
         }
-        // An app drawing its own context menu into the list (draws_own_popovers) gets the
-        // same occlusion for it: the menu rect clamps list text beneath, and the menu's own
-        // labels are exempt because they carry bounds equal to the rect.
-        if self.inner.as_ref().unwrap().draws_own_popovers() && crate::widget::context_menu::is_visible() {
+        // The global context menu draws into the app's display list (the render-only xdg
+        // popup is gone), so it gets the same occlusion: the menu rect clamps list text
+        // beneath, and the menu's own labels are exempt because they carry bounds equal
+        // to the rect.
+        if crate::widget::context_menu::is_visible() {
             dl_overlay_rects.push((
                 crate::widget::context_menu::x(),
                 crate::widget::context_menu::y(),
@@ -2137,139 +2106,7 @@ impl<A: Application> EngineState<A> {
 
         adapter.queue.submit(std::iter::once(encoder.finish()));
         output.present();
-        
-        if let Some(ref mut popup) = self.active_popup {
-            if popup.configured {
-                let mut collector = crate::layout::PopoverCollector::new();
-                self.inner.as_mut().unwrap().render_popovers(&mut collector);
- 
-                let mut verts = Vec::new();
-                for &(color, qx, qy, qw, qh) in &collector.rects {
-                    let qx_local = qx - popup.x;
-                    let qy_local = qy - popup.y;
-                    verts.extend(quad_vertices(qx_local, qy_local, qw, qh, popup.logical_width, popup.logical_height, color));
-                }
- 
-                let vertex_count = verts.len() as u32;
-                if vertex_count > 0 {
-                    let data = bytemuck::cast_slice(&verts);
-                    let needed = data.len() as wgpu::BufferAddress;
-                    let mut vbuf = popup.vertex_buffer.as_ref();
-                    if vbuf.map_or(true, |v| needed > v.size()) {
-                        let new_vbuf = adapter.device.create_buffer(&wgpu::BufferDescriptor {
-                            label: Some("Popup Vertex Buffer"),
-                            size: needed,
-                            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                            mapped_at_creation: false,
-                        });
-                        popup.vertex_buffer = Some(new_vbuf);
-                        vbuf = popup.vertex_buffer.as_ref();
-                    }
-                    adapter.queue.write_buffer(vbuf.unwrap(), 0, data);
-                }
- 
-                let mut text_items = Vec::new();
-                for (content, size, tx, ty, color, font, bounds) in collector.texts {
-                    let tx_local = tx - popup.x;
-                    let ty_local = ty - popup.y;
-                    text_items.push(TextItem {
-                        buffer: make_text_buffer_with_font(&mut adapter.font_system, &content, size, font.as_deref()),
-                        x: tx_local,
-                        y: ty_local,
-                        color: glyphon::Color::rgb(
-                            (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,
-                        ),
-                        bounds,
-                    });
-                }
- 
-                let scale_f32 = scale_factor as f32;
-                let bounds = TextBounds {
-                    left: 0,
-                    top: 0,
-                    right: (popup.logical_width * scale_f32) as i32,
-                    bottom: (popup.logical_height * scale_f32) as i32,
-                };
-                let areas: Vec<TextArea<'_>> = text_items.iter().map(|ti| TextArea {
-                    buffer: &ti.buffer,
-                    left: (ti.x * scale_f32).round(),
-                    top: (ti.y * scale_f32).round(),
-                    scale: 1.0,
-                    bounds,
-                    default_color: ti.color,
-                    custom_glyphs: &[],
-                }).collect();
- 
-                popup.viewport.update(&adapter.queue, Resolution {
-                    width: (popup.logical_width * scale_f32) as u32,
-                    height: (popup.logical_height * scale_f32) as u32,
-                });
- 
-                adapter.text_renderer.prepare(
-                    &adapter.device,
-                    &adapter.queue,
-                    &mut adapter.font_system,
-                    &mut adapter.text_atlas,
-                    &popup.viewport,
-                    areas,
-                    &mut adapter.swash_cache,
-                ).unwrap();
- 
-                let popup_output = match popup.wgpu_surface.get_current_texture() {
-                    Ok(t) => t,
-                    Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
-                        popup.wgpu_surface.configure(&adapter.device, &popup.config);
-                        match popup.wgpu_surface.get_current_texture() {
-                            Ok(t) => t,
-                            Err(e) => {
-                                log::error!("Popup surface error after configure: {e:?}");
-                                return;
-                            }
-                        }
-                    }
-                    Err(wgpu::SurfaceError::Timeout) => return,
-                    Err(e) => {
-                        log::error!("Popup surface error: {e:?}");
-                        return;
-                    }
-                };
-                let popup_view = popup_output.texture.create_view(&wgpu::TextureViewDescriptor::default());
-                let mut popup_encoder = adapter.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
-                    label: Some("Popup Encoder"),
-                });
- 
-                {
-                    let mut pass = popup_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                        label: Some("Popup Render Pass"),
-                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                            view: &popup_view,
-                            resolve_target: None,
-                            ops: wgpu::Operations {
-                                load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
-                                store: wgpu::StoreOp::Store,
-                            },
-                        })],
-                        depth_stencil_attachment: None,
-                        timestamp_writes: None,
-                        occlusion_query_set: None,
-                    });
- 
-                    if vertex_count > 0 {
-                        pass.set_pipeline(render_pipeline);
-                        pass.set_vertex_buffer(0, popup.vertex_buffer.as_ref().unwrap().slice(..));
-                        pass.draw(0..vertex_count, 0..1);
-                    }
- 
-                    adapter.text_renderer.render(&adapter.text_atlas, &popup.viewport, &mut pass).unwrap();
-                }
- 
-                adapter.queue.submit(std::iter::once(popup_encoder.finish()));
-                popup_output.present();
-            }
-        }
- 
+
         adapter.text_atlas.trim();
     }
 }
@@ -2518,16 +2355,9 @@ impl<A: Application> PointerHandler for EngineState<A> {
 
         for event in events {
             let (x, y) = event.position;
-            let mut lx = x as f32;
-            let mut ly = y as f32;
-            
-            if let Some(ref popup) = self.active_popup {
-                if event.surface == *popup.sctk_popup.wl_surface() {
-                    lx += popup.x;
-                    ly += popup.y;
-                }
-            }
-            
+            let lx = x as f32;
+            let ly = y as f32;
+
             self.cursor_pos = (lx, ly);
             match &event.kind {
                 PointerEventKind::Enter { .. } => {
@@ -2942,37 +2772,9 @@ impl<A: Application> wayland_client::Dispatch<crate::protocol::zcce_inspector_v1
     ) {}
 }
 
-impl<A: Application> PopupHandler for EngineState<A> {
-    fn configure(
-        &mut self,
-        _conn: &Connection,
-        _qh: &QueueHandle<Self>,
-        _popup: &Popup,
-        _config: PopupConfigure,
-    ) {
-        if let Some(ref mut p) = self.active_popup {
-            p.configured = true;
-        }
-        self.redraw = true;
-    }
-
-    fn done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _popup: &Popup) {
-        for popover_ptr in crate::widget::popovers::get_active() {
-            unsafe {
-                let popover = &mut *(popover_ptr as *mut dyn crate::widget::Element);
-                popover.unfocus();
-            }
-        }
-        crate::widget::context_menu::hide();
-        self.active_popup = None;
-        self.redraw = true;
-    }
-}
-
 delegate_compositor!(@<A: Application> EngineState<A>);
 delegate_xdg_shell!(@<A: Application> EngineState<A>);
 delegate_xdg_window!(@<A: Application> EngineState<A>);
-delegate_xdg_popup!(@<A: Application> EngineState<A>);
 delegate_layer!(@<A: Application> EngineState<A>);
 delegate_shm!(@<A: Application> EngineState<A>);
 delegate_seat!(@<A: Application> EngineState<A>);
@@ -3118,7 +2920,6 @@ pub fn run<A: Application>() {
         logo_pressed: false,
         pressed_key: None,
         sender,
-        active_popup: None,
         current_cursor_icon: None,
         qh: qh.clone(),
         just_configured: false,
@@ -3290,126 +3091,6 @@ pub fn run<A: Application>() {
             last_title = current_title;
         }
 
-        // Apps that draw popovers/context menu into their own display list (Phase 6) opt out
-        // of the render-only popup surface entirely.
-        let draws_own = engine_state.inner.as_ref().map_or(false, |a| a.draws_own_popovers());
-        let active_popovers = crate::widget::popovers::get_active();
-        let context_menu_visible = crate::widget::context_menu::is_visible();
-
-        let mut active_popover_rect = None;
-        if !draws_own && !active_popovers.is_empty() {
-            let popover_widget = unsafe { &*active_popovers[0] };
-            active_popover_rect = popover_widget.popover_rect();
-        }
-
-        if active_popover_rect.is_some() || (context_menu_visible && !draws_own) {
-            let (px, py, mut pw, mut ph, is_context_menu) = if let Some((x, y, w, h)) = active_popover_rect {
-                (x, y, w.max(1.0), h.max(1.0), false)
-            } else {
-                (
-                    crate::widget::context_menu::x(),
-                    crate::widget::context_menu::y(),
-                    crate::widget::context_menu::w().max(1.0),
-                    crate::widget::context_menu::h().max(1.0),
-                    true,
-                )
-            };
-            pw = pw.ceil();
-            ph = ph.ceil();
-            if engine_state.active_popup.is_none() && engine_state.window.is_some() {
-                let positioner = XdgPositioner::new(&engine_state.xdg_shell_state).unwrap();
-                positioner.set_size(pw as i32, ph as i32);
-                
-                if !is_context_menu {
-                    let popover_widget = unsafe { &*active_popovers[0] };
-                    let (rx, ry, rw, rh) = popover_widget.rect();
-                    let scroll_y = crate::widget::hover_animation::get_scroll_offset();
-                    let screen_ry = ry - scroll_y;
-                    let lw = engine_state.logical_width as i32;
-                    let lh = engine_state.logical_height as i32;
-                    let ax = (rx as i32).clamp(0, (lw - 1).max(0));
-                    let ay = (screen_ry as i32).clamp(0, (lh - 1).max(0));
-                    let aw = (rw as i32).clamp(1, (lw - ax).max(1));
-                    let ah = (rh as i32).clamp(1, (lh - ay).max(1));
-                    positioner.set_anchor_rect(ax, ay, aw, ah);
-                    positioner.set_anchor(Anchor::BottomLeft);
-                    positioner.set_gravity(Gravity::BottomRight);
-                } else {
-                    let lw = engine_state.logical_width as i32;
-                    let lh = engine_state.logical_height as i32;
-                    let ax = (px as i32).clamp(0, (lw - 1).max(0));
-                    let ay = (py as i32).clamp(0, (lh - 1).max(0));
-                    let aw = 1.clamp(1, (lw - ax).max(1));
-                    let ah = 1.clamp(1, (lh - ay).max(1));
-                    positioner.set_anchor_rect(ax, ay, aw, ah);
-                    positioner.set_anchor(Anchor::TopLeft);
-                    positioner.set_gravity(Gravity::BottomRight);
-                }
-                positioner.set_constraint_adjustment(
-                    ConstraintAdjustment::SlideX | ConstraintAdjustment::SlideY
-                );
-                
-                let parent_xdg_surface = XdgSurface::xdg_surface(engine_state.window.as_ref().unwrap());
-                let sctk_popup = Popup::new(
-                    parent_xdg_surface,
-                    &positioner,
-                    &engine_state.qh,
-                    &engine_state.compositor_state,
-                    &engine_state.xdg_shell_state,
-                ).unwrap();
-                sctk_popup.wl_surface().set_buffer_scale(engine_state.scale_factor as i32);
-                // Render-only popup: give it an EMPTY input region so pointer events pass
-                // through to the main surface beneath (which draws and hit-tests the popover
-                // content itself). With the default full input region the popup swallowed
-                // every click on an open menu — item clicks never reached the app.
-                {
-                    let compositor = engine_state.compositor_state.wl_compositor();
-                    let empty_region = compositor.create_region(&engine_state.qh, ());
-                    sctk_popup.wl_surface().set_input_region(Some(&empty_region));
-                    empty_region.destroy();
-                }
-
-                    let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
-                    let surface_ptr = sctk_popup.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
-                    let wayland_handle = Box::leak(Box::new(WaylandSurfaceHandle {
-                        display_ptr,
-                        surface_ptr,
-                    }));
-                    let instance = &engine_state.wgpu_adapter.as_ref().unwrap().instance;
-                    let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create popup wgpu surface");
-                    
-                    let device = &engine_state.wgpu_adapter.as_ref().unwrap().device;
-                    let main_config = &engine_state.wgpu_adapter.as_ref().unwrap().config;
-                    
-                    let scale_f32 = engine_state.scale_factor as f32;
-                    let mut popup_config = main_config.clone();
-                    popup_config.width = ((pw * scale_f32) as u32).max(1);
-                    popup_config.height = ((ph * scale_f32) as u32).max(1);
-                    wgpu_surface.configure(device, &popup_config);
-                    
-                    let cache = Cache::new(device);
-                    let viewport = Viewport::new(device, &cache);
-                    
-                    engine_state.active_popup = Some(ActivePopup {
-                        sctk_popup,
-                        wgpu_surface,
-                        config: popup_config,
-                        logical_width: pw,
-                        logical_height: ph,
-                        configured: false,
-                        viewport,
-                        x: px,
-                        y: py,
-                        vertex_buffer: None,
-                    });
-                }
-        } else {
-            if engine_state.active_popup.is_some() {
-                engine_state.active_popup = None;
-                engine_state.redraw = true;
-            }
-        }
-
         if engine_state.redraw && !engine_state.frame_callback_pending {
             engine_state.redraw = false;
             if engine_state.first_configure_received {
diff --git a/src/engine.rs b/src/engine.rs
index d036b44..e6c64f7 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -1,5 +1,5 @@
 pub use crate::backend::window_runner::{
-    ActivePopup, Vertex, LineCap, WindowSettings, LogicalPosition, LogicalSize,
+    Vertex, LineCap, WindowSettings, LogicalPosition, LogicalSize,
     RenderContext, Application, PressedKey, EngineState, run,
     LayerSettings, LayerKind, LayerAnchor, LayerKeyboardInteractivity,
     quad_vertices, quad_vertices_with_clip, quad_vertices_clipped, line_vertices,