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

commit14d686508b7105049937bad6035bf5811c25326b
parent2cb9b4afb8
authorLucas Galante <[email protected]>
date2026-06-01 11:49
Introduce strongly-typed LogicalPosition and LogicalSize to avoid scaling mismatches

 src/engine.rs | 1026 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs    |    1 +
 2 files changed, 1027 insertions(+)

diff --git a/src/engine.rs b/src/engine.rs
new file mode 100644
index 0000000..a269f5b
--- /dev/null
+++ b/src/engine.rs
@@ -0,0 +1,1026 @@
+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,
+    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},
+};
+use wayland_client::{
+    globals::{registry_queue_init, GlobalList},
+    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface, wl_registry},
+    Connection, QueueHandle, Proxy,
+};
+use calloop::EventLoop;
+use calloop_wayland_source::WaylandSource;
+use glyphon::{
+    Cache, FontSystem, Resolution, SwashCache, TextArea, TextAtlas,
+    TextBounds, TextRenderer, Viewport,
+};
+use crate::widget::{TextItem, MouseButton, ElementState, MouseScrollDelta, KeyEvent, Key, NamedKey};
+use crate::wayland::{WaylandSurfaceHandle, detect_scale_factor};
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct Vertex {
+    pub position: [f32; 2],
+    pub color: [f32; 4],
+}
+
+impl Vertex {
+    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+        0 => Float32x2,
+        1 => Float32x4,
+    ];
+
+    pub 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,
+        }
+    }
+}
+
+pub fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4]) -> [Vertex; 6] {
+    let x0 = (x / sw) * 2.0 - 1.0;
+    let y0 = 1.0 - (y / sh) * 2.0;
+    let x1 = ((x + w) / sw) * 2.0 - 1.0;
+    let y1 = 1.0 - ((y + h) / sh) * 2.0;
+    [
+        Vertex { position: [x0, y0], color: c },
+        Vertex { position: [x1, y0], color: c },
+        Vertex { position: [x0, y1], color: c },
+        Vertex { position: [x1, y0], color: c },
+        Vertex { position: [x1, y1], color: c },
+        Vertex { position: [x0, y1], color: c },
+    ]
+}
+
+#[derive(Debug, Clone)]
+pub struct WindowSettings {
+    pub title: String,
+    pub app_id: String,
+    pub width: u32,
+    pub height: u32,
+    pub fullscreen: bool,
+    pub min_size: Option<(u32, u32)>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LogicalPosition {
+    pub x: f32,
+    pub y: f32,
+}
+
+impl LogicalPosition {
+    pub fn new(x: f32, y: f32) -> Self {
+        Self { x, y }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct LogicalSize {
+    pub width: f32,
+    pub height: f32,
+}
+
+impl LogicalSize {
+    pub fn new(width: f32, height: f32) -> Self {
+        Self { width, height }
+    }
+}
+
+pub struct RenderContext<'a> {
+    pub font_system: &'a mut FontSystem,
+}
+
+pub trait Application: Sized + 'static {
+    type Message: Send + Clone + 'static;
+
+    fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self;
+    fn settings(&self) -> WindowSettings;
+    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
+    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64);
+    fn overlay_quads(&mut self, _quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, _size: LogicalSize, _scale: f64) {}
+    fn text_items(&self) -> &[TextItem];
+    
+    fn clear_color(&self) -> [f32; 4] {
+        [0.0, 0.0, 0.0, 0.0]
+    }
+    
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool);
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message>;
+    fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool);
+    fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message>;
+}
+
+pub struct PressedKey {
+    pub logical_key: Key,
+    pub text: Option<String>,
+    pub first_pressed: Instant,
+    pub last_repeated: Instant,
+}
+
+fn is_repeatable_key(key: &Key) -> bool {
+    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,
+    }
+}
+
+pub struct EngineState<A: Application> {
+    pub registry_state: RegistryState,
+    pub compositor_state: CompositorState,
+    pub xdg_shell_state: XdgShell,
+    pub shm_state: Shm,
+    pub seat_state: SeatState,
+    pub output_state: OutputState,
+    pub seats: Vec<wl_seat::WlSeat>,
+    pub pointer: Option<ThemedPointer>,
+    pub keyboard: Option<wl_keyboard::WlKeyboard>,
+    
+    pub window: Option<XdgWindow>,
+    pub surface: Option<wl_surface::WlSurface>,
+    
+    pub inner: A,
+    
+    pub wgpu_surface: Option<wgpu::Surface<'static>>,
+    pub device: Option<wgpu::Device>,
+    pub queue: Option<wgpu::Queue>,
+    pub config: Option<wgpu::SurfaceConfiguration>,
+    pub render_pipeline: Option<wgpu::RenderPipeline>,
+    pub vertex_buffer: Option<wgpu::Buffer>,
+    pub vertex_count: u32,
+    pub overlay_vertex_buffer: Option<wgpu::Buffer>,
+    pub overlay_vertex_count: u32,
+    
+    pub font_system: FontSystem,
+    pub swash_cache: SwashCache,
+    pub text_atlas: Option<TextAtlas>,
+    pub text_renderer: Option<TextRenderer>,
+    pub text_viewport: Option<Viewport>,
+    
+    pub scale_factor: f64,
+    pub logical_width: f32,
+    pub logical_height: f32,
+    
+    pub exit: bool,
+    pub redraw: bool,
+    pub first_configure_received: bool,
+    pub ctrl_pressed: bool,
+    pub shift_pressed: bool,
+    pub pressed_key: Option<PressedKey>,
+    pub sender: calloop::channel::Sender<A::Message>,
+}
+
+impl<A: Application> EngineState<A> {
+    pub async fn init_gpu(&mut self, conn: &Connection, width_logical: f32, height_logical: f32) {
+        let s = self.scale_factor as f32;
+        let pw = (width_logical * s) as u32;
+        let ph = (height_logical * s) as u32;
+        
+        let surface = self.surface.as_ref().expect("surface missing");
+        
+        let wayland_handle = Box::leak(Box::new(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 instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
+            backends: wgpu::Backends::VULKAN,
+            ..Default::default()
+        });
+        
+        let wgpu_surface = instance.create_surface(wayland_handle).expect("failed to create wgpu surface");
+        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
+            power_preference: wgpu::PowerPreference::LowPower,
+            compatible_surface: Some(&wgpu_surface),
+            force_fallback_adapter: false,
+        }).await.expect("failed to request adapter");
+        
+        let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
+            label: Some("GPU Device"),
+            required_features: wgpu::Features::empty(),
+            required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
+            memory_hints: wgpu::MemoryHints::MemoryUsage,
+        }, None).await.expect("failed to request device");
+        
+        let config = wgpu_surface.get_default_config(&adapter, pw.max(1), ph.max(1)).expect("failed to get surface configuration");
+        
+        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Shader"),
+            source: wgpu::ShaderSource::Wgsl(crate::SHADER.into()),
+        });
+        
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("Pipeline Layout"),
+            bind_group_layouts: &[],
+            push_constant_ranges: &[],
+        });
+        
+        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Render Pipeline"),
+            layout: Some(&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,
+                front_face: wgpu::FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                unclipped_depth: false,
+                conservative: false,
+                strip_index_format: None,
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false },
+            multiview: None,
+            cache: None,
+        });
+        
+        let cache = Cache::new(&device);
+        let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+        let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+        let mut text_viewport = Viewport::new(&device, &cache);
+        text_viewport.update(&queue, Resolution { width: pw, height: ph });
+        
+        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 overlay_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some("Overlay Vertex Buffer"),
+            size: 1,
+            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+            mapped_at_creation: false,
+        });
+        
+        self.wgpu_surface = Some(wgpu_surface);
+        self.device = Some(device);
+        self.queue = Some(queue);
+        self.config = Some(config);
+        self.render_pipeline = Some(render_pipeline);
+        self.text_atlas = Some(text_atlas);
+        self.text_renderer = Some(text_renderer);
+        self.text_viewport = Some(text_viewport);
+        self.vertex_buffer = Some(vertex_buffer);
+        self.overlay_vertex_buffer = Some(overlay_vertex_buffer);
+        self.logical_width = width_logical;
+        self.logical_height = height_logical;
+    }
+    
+    pub fn resize(&mut self, w: f32, h: f32) {
+        if w > 0.0 && h > 0.0 {
+            self.logical_width = w;
+            self.logical_height = h;
+            if let (Some(device), Some(surface), Some(config)) = (&self.device, &self.wgpu_surface, &mut self.config) {
+                config.width = (w as f64 * self.scale_factor) as u32;
+                config.height = (h as f64 * self.scale_factor) as u32;
+                surface.configure(device, config);
+            }
+        }
+    }
+    
+    pub fn render(&mut self) {
+        let logical_w = self.logical_width;
+        let logical_h = self.logical_height;
+        let scale_factor = self.scale_factor;
+        
+        let mut quads = Vec::new();
+        self.inner.view(&mut quads, LogicalSize::new(logical_w, logical_h), scale_factor);
+        
+        let device = self.device.as_ref().unwrap();
+        let queue = self.queue.as_ref().unwrap();
+        let wgpu_surface = self.wgpu_surface.as_ref().unwrap();
+        let render_pipeline = self.render_pipeline.as_ref().unwrap();
+        let text_renderer = self.text_renderer.as_mut().unwrap();
+        let text_atlas = self.text_atlas.as_mut().unwrap();
+        let text_viewport = self.text_viewport.as_mut().unwrap();
+        let config = self.config.as_ref().unwrap();
+        
+        // 1. Build and upload vertex buffer
+        let mut verts = Vec::new();
+        for &(qx, qy, qw, qh, qc) in &quads {
+            verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
+        }
+        self.vertex_count = verts.len() as u32;
+        if self.vertex_count > 0 {
+            let data = bytemuck::cast_slice(&verts);
+            let needed = data.len() as wgpu::BufferAddress;
+            let mut vbuf = self.vertex_buffer.as_ref().unwrap();
+            if needed > vbuf.size() {
+                let new_vbuf = device.create_buffer(&wgpu::BufferDescriptor {
+                    label: Some("Vertex Buffer"),
+                    size: needed,
+                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                    mapped_at_creation: false,
+                });
+                self.vertex_buffer = Some(new_vbuf);
+                vbuf = self.vertex_buffer.as_ref().unwrap();
+            }
+            queue.write_buffer(vbuf, 0, data);
+        }
+
+        // 1b. Build and upload overlay vertex buffer
+        let mut overlay_quads = Vec::new();
+        self.inner.overlay_quads(&mut overlay_quads, LogicalSize::new(logical_w, logical_h), scale_factor);
+        let mut overlay_verts = Vec::new();
+        for &(qx, qy, qw, qh, qc) in &overlay_quads {
+            overlay_verts.extend(quad_vertices(qx, qy, qw, qh, logical_w, logical_h, qc));
+        }
+        self.overlay_vertex_count = overlay_verts.len() as u32;
+        if self.overlay_vertex_count > 0 {
+            let data = bytemuck::cast_slice(&overlay_verts);
+            let needed = data.len() as wgpu::BufferAddress;
+            let mut ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
+            if needed > ovbuf.size() {
+                let new_ovbuf = device.create_buffer(&wgpu::BufferDescriptor {
+                    label: Some("Overlay Vertex Buffer"),
+                    size: needed,
+                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                    mapped_at_creation: false,
+                });
+                self.overlay_vertex_buffer = Some(new_ovbuf);
+                ovbuf = self.overlay_vertex_buffer.as_ref().unwrap();
+            }
+            queue.write_buffer(ovbuf, 0, data);
+        }
+        
+        // 2. Prepare text
+        let scale_f32 = scale_factor as f32;
+        let pw = (logical_w * scale_f32) as u32;
+        let ph = (logical_h * scale_f32) as u32;
+        text_viewport.update(queue, Resolution { width: pw, height: ph });
+        
+        let bounds = TextBounds { left: 0, top: 0, right: pw as i32, bottom: ph as i32 };
+        let text_items = self.inner.text_items();
+        let areas: Vec<TextArea> = text_items.iter().map(|ti| TextArea {
+            buffer: &ti.buffer,
+            left: ti.x * scale_f32,
+            top: ti.y * scale_f32,
+            scale: scale_f32,
+            bounds,
+            default_color: ti.color,
+            custom_glyphs: &[],
+        }).collect();
+        
+        text_renderer.prepare(device, queue, &mut self.font_system, text_atlas, text_viewport, areas, &mut self.swash_cache).unwrap();
+        
+        // 3. Render Pass
+        let output = match wgpu_surface.get_current_texture() {
+            Ok(t) => t,
+            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+                wgpu_surface.configure(device, config);
+                return;
+            }
+            Err(wgpu::SurfaceError::Timeout) => return,
+            Err(e) => {
+                eprintln!("Surface error: {e:?}");
+                return;
+            }
+        };
+        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
+        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("Encoder"),
+        });
+        
+        {
+            let cc = self.inner.clear_color();
+            let r_clear = (cc[0] as f64).powf(2.2);
+            let g_clear = (cc[1] as f64).powf(2.2);
+            let b_clear = (cc[2] as f64).powf(2.2);
+            let a_clear = cc[3] as f64;
+            
+            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: r_clear,
+                            g: g_clear,
+                            b: b_clear,
+                            a: a_clear,
+                        }),
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+            
+            if self.vertex_count > 0 {
+                pass.set_pipeline(render_pipeline);
+                pass.set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..));
+                pass.draw(0..self.vertex_count, 0..1);
+            }
+            
+            text_renderer.render(text_atlas, text_viewport, &mut pass).unwrap();
+
+            if self.overlay_vertex_count > 0 {
+                pass.set_pipeline(render_pipeline);
+                pass.set_vertex_buffer(0, self.overlay_vertex_buffer.as_ref().unwrap().slice(..));
+                pass.draw(0..self.overlay_vertex_count, 0..1);
+            }
+        }
+        
+        queue.submit(std::iter::once(encoder.finish()));
+        output.present();
+        text_atlas.trim();
+    }
+}
+
+impl<A: Application> CompositorHandler for EngineState<A> {
+    fn scale_factor_changed(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _surface: &wl_surface::WlSurface,
+        scale_factor: i32,
+    ) {
+        _surface.set_buffer_scale(scale_factor);
+        self.scale_factor = scale_factor as f64;
+        self.resize(self.logical_width, self.logical_height);
+        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 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,
+    ) {}
+}
+
+impl<A: Application> OutputHandler for EngineState<A> {
+    fn output_state(&mut self) -> &mut OutputState {
+        &mut self.output_state
+    }
+    
+    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<A: Application> ShmHandler for EngineState<A> {
+    fn shm_state(&mut self) -> &mut Shm {
+        &mut self.shm_state
+    }
+}
+
+impl<A: Application> ProvidesRegistryState for EngineState<A> {
+    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(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _name: u32,
+        _interface: &str,
+    ) {}
+}
+
+impl<A: Application> WindowHandler for EngineState<A> {
+    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();
+            self.resize(width as f32, height as f32);
+        }
+        self.redraw = true;
+        self.first_configure_received = true;
+    }
+    
+    fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
+        self.exit = true;
+    }
+}
+
+impl<A: Application> SeatHandler for EngineState<A> {
+    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;
+        }
+        if capability == Capability::Keyboard {
+            self.keyboard = None;
+        }
+    }
+    
+    fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
+        self.seats.retain(|s| s != &seat);
+    }
+}
+
+impl<A: Application> PointerHandler for EngineState<A> {
+    fn pointer_frame(
+        &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;
+        for event in events {
+            let (x, y) = event.position;
+            let lx = x as f32;
+            let ly = 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 { .. } => {
+                    let mut rebuild = false;
+                    self.inner.handle_pointer_move(LogicalPosition::new(lx, ly), &mut rebuild);
+                    if rebuild {
+                        self.redraw = true;
+                    }
+                }
+                PointerEventKind::Press { button, .. } => {
+                    let btn = match *button {
+                        272 => MouseButton::Left,
+                        273 => MouseButton::Right,
+                        274 => MouseButton::Middle,
+                        _ => continue,
+                    };
+                    let mut rebuild = false;
+                    if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Pressed, LogicalPosition::new(lx, ly), &mut rebuild) {
+                        let mut update_rebuild = false;
+                        self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+                        if update_rebuild {
+                            rebuild = true;
+                        }
+                    }
+                    if rebuild {
+                        self.redraw = true;
+                    }
+                }
+                PointerEventKind::Release { button, .. } => {
+                    let btn = match *button {
+                        272 => MouseButton::Left,
+                        273 => MouseButton::Right,
+                        274 => MouseButton::Middle,
+                        _ => continue,
+                    };
+                    let mut rebuild = false;
+                    if let Some(msg) = self.inner.handle_mouse_input(btn, ElementState::Released, LogicalPosition::new(lx, ly), &mut rebuild) {
+                        let mut update_rebuild = false;
+                        self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+                        if update_rebuild {
+                            rebuild = true;
+                        }
+                    }
+                    if rebuild {
+                        self.redraw = true;
+                    }
+                }
+                PointerEventKind::Axis { horizontal, vertical, .. } => {
+                    let h_scroll = horizontal.absolute as f32;
+                    let v_scroll = vertical.absolute as f32;
+                    let delta = MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
+                    let mut rebuild = false;
+                    self.inner.handle_mouse_wheel(&delta, LogicalPosition::new(lx, ly), &mut rebuild);
+                    if rebuild {
+                        self.redraw = true;
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl<A: Application> KeyboardHandler for EngineState<A> {
+    fn enter(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _keyboard: &wl_keyboard::WlKeyboard,
+        _surface: &wl_surface::WlSurface,
+        _serial: u32,
+        _raw_modifiers: &[u32],
+        _keysyms: &[xkeysym::Keysym],
+    ) {}
+    
+    fn leave(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _keyboard: &wl_keyboard::WlKeyboard,
+        _surface: &wl_surface::WlSurface,
+        _serial: u32,
+    ) {}
+    
+    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, 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, 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;
+    }
+}
+
+impl<A: Application> EngineState<A> {
+    fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: ElementState) {
+        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 {
+                    return;
+                }
+            }
+        };
+
+        let custom_event = KeyEvent {
+            state,
+            logical_key,
+            text: event.utf8.clone(),
+            repeat: false,
+            ctrl: self.ctrl_pressed,
+            shift: self.shift_pressed,
+        };
+
+        if state == 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: Instant::now(),
+                    last_repeated: Instant::now(),
+                });
+            } else {
+                self.pressed_key = None;
+            }
+        } else if state == ElementState::Released {
+            if let Some(ref pk) = self.pressed_key {
+                if pk.logical_key == custom_event.logical_key {
+                    self.pressed_key = None;
+                }
+            }
+        }
+
+        let mut rebuild = false;
+        if let Some(msg) = self.inner.handle_key_input(&custom_event, &mut rebuild) {
+            let mut update_rebuild = false;
+            self.inner.update(msg, &mut update_rebuild, &mut self.exit);
+            if update_rebuild {
+                rebuild = true;
+            }
+        }
+        if rebuild {
+            self.redraw = true;
+        }
+    }
+}
+
+impl<A: Application> wayland_client::Dispatch<wl_registry::WlRegistry, GlobalList, Self> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &wl_registry::WlRegistry,
+        _event: wl_registry::Event,
+        _data: &GlobalList,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::zclear_inspector_v1::ZclearInspectorV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::zclear_inspector_v1::ZclearInspectorV1,
+        _event: crate::protocol::zclear_inspector_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
+// Delegate macros for generic EngineState
+delegate_compositor!(@<A: Application> EngineState<A>);
+delegate_xdg_shell!(@<A: Application> EngineState<A>);
+delegate_xdg_window!(@<A: Application> EngineState<A>);
+delegate_shm!(@<A: Application> EngineState<A>);
+delegate_seat!(@<A: Application> EngineState<A>);
+delegate_pointer!(@<A: Application> EngineState<A>);
+delegate_keyboard!(@<A: Application> EngineState<A>);
+delegate_registry!(@<A: Application> EngineState<A>);
+delegate_output!(@<A: Application> EngineState<A>);
+
+pub fn run<A: Application>() {
+    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 (sender, channel) = calloop::channel::channel::<A::Message>();
+
+    let inner = A::new(&qh, sender.clone());
+    let settings = inner.settings();
+
+    let font_system = FontSystem::new();
+    let swash_cache = SwashCache::new();
+
+    let mut engine_state = EngineState {
+        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,
+        inner,
+        wgpu_surface: None,
+        device: None,
+        queue: None,
+        config: None,
+        render_pipeline: None,
+        vertex_buffer: None,
+        vertex_count: 0,
+        overlay_vertex_buffer: None,
+        overlay_vertex_count: 0,
+        font_system,
+        swash_cache,
+        text_atlas: None,
+        text_renderer: None,
+        text_viewport: None,
+        scale_factor: 1.0,
+        logical_width: settings.width as f32,
+        logical_height: settings.height as f32,
+        exit: false,
+        redraw: false,
+        first_configure_received: false,
+        ctrl_pressed: false,
+        shift_pressed: false,
+        pressed_key: None,
+        sender,
+    };
+
+    event_queue.roundtrip(&mut engine_state).unwrap();
+
+    let scale = detect_scale_factor(&engine_state.output_state);
+    engine_state.scale_factor = scale;
+
+    let surface = engine_state.compositor_state.create_surface(&qh);
+    surface.set_buffer_scale(scale as i32);
+    let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
+    window.set_title(&settings.title);
+    window.set_app_id(&settings.app_id);
+    if settings.fullscreen {
+        window.set_fullscreen(None);
+    }
+    if let Some((min_w, min_h)) = settings.min_size {
+        window.set_min_size(Some((min_w, min_h)));
+    }
+    window.commit();
+
+    engine_state.window = Some(window);
+    engine_state.surface = Some(surface);
+
+    pollster::block_on(engine_state.init_gpu(&conn, settings.width as f32, settings.height as f32));
+
+    let mut event_loop = EventLoop::try_new().unwrap();
+    let loop_handle = event_loop.handle();
+    WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
+
+    loop_handle.insert_source(channel, |event, _metadata, app_state: &mut EngineState<A>| {
+        if let calloop::channel::Event::Msg(msg) = event {
+            let mut rebuild = false;
+            app_state.inner.update(msg, &mut rebuild, &mut app_state.exit);
+            if rebuild {
+                app_state.redraw = true;
+            }
+        }
+    }).unwrap();
+
+    const KEY_REPEAT_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
+    const KEY_REPEAT_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
+
+    let mut last_tick = std::time::Instant::now();
+    loop {
+        event_loop.dispatch(std::time::Duration::from_millis(16), &mut engine_state).unwrap();
+        if engine_state.exit {
+            break;
+        }
+
+        let now = std::time::Instant::now();
+        let mut dt = now.duration_since(last_tick).as_secs_f32();
+        last_tick = now;
+        if dt > 0.1 {
+            dt = 0.1;
+        }
+
+        let mut rebuild = false;
+        engine_state.inner.tick(dt, &mut rebuild);
+        if rebuild {
+            engine_state.redraw = true;
+        }
+
+        if let Some(ref mut pk) = engine_state.pressed_key {
+            let now = std::time::Instant::now();
+            if now.duration_since(pk.first_pressed) >= KEY_REPEAT_DELAY {
+                if now.duration_since(pk.last_repeated) >= KEY_REPEAT_INTERVAL {
+                    pk.last_repeated = now;
+                    let custom_event = KeyEvent {
+                        state: ElementState::Pressed,
+                        logical_key: pk.logical_key.clone(),
+                        text: pk.text.clone(),
+                        repeat: true,
+                        ctrl: engine_state.ctrl_pressed,
+                        shift: engine_state.shift_pressed,
+                    };
+                    let mut key_rebuild = false;
+                    if let Some(msg) = engine_state.inner.handle_key_input(&custom_event, &mut key_rebuild) {
+                        let mut update_rebuild = false;
+                        engine_state.inner.update(msg, &mut update_rebuild, &mut engine_state.exit);
+                        if update_rebuild {
+                            key_rebuild = true;
+                        }
+                    }
+                    if key_rebuild {
+                        engine_state.redraw = true;
+                    }
+                }
+            }
+        }
+
+        if engine_state.redraw {
+            engine_state.redraw = false;
+            if engine_state.first_configure_received {
+                engine_state.render();
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3a75d7b..8b2c731 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@ pub mod widget;
 pub mod layout;
 pub mod wayland;
 pub mod protocol;
+pub mod engine;
 
 pub mod colors {
     pub use crate::color::*;