git.lucas.co / cce-notifier
notification daemon
git clone https://git.lucas.co/cce-notifier.git

commit31940a830059623f0ac83eac76723ff1b599367e
parent67085c8d98
authorLucas Galante <[email protected]>
date2026-05-25 20:36
Update system configuration and interface modules

 Cargo.lock  |   2 +-
 Cargo.toml  |   2 +-
 src/main.rs | 293 +++++++++++++++++++++++++++++++++++++-----------------------
 3 files changed, 185 insertions(+), 112 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4739b50..4e3d3bb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -305,7 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 [[package]]
-name = "clear-notifier"
+name = "clear-notification-daemon"
 version = "0.1.0"
 dependencies = [
  "bytemuck",
diff --git a/Cargo.toml b/Cargo.toml
index 66b01cd..b75d161 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "clear-notifier"
+name = "clear-notification-daemon"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/src/main.rs b/src/main.rs
index d553ef5..ce7faec 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -98,6 +98,15 @@ struct TextItem {
 
 // ── NotificationApp ──
 
+struct RendererResources {
+    render_pipeline: wgpu::RenderPipeline,
+    font_system: FontSystem,
+    swash_cache: SwashCache,
+    text_atlas: TextAtlas,
+    text_renderer: TextRenderer,
+    cache: Cache,
+}
+
 struct NotificationApp {
     window: XdgWindow,
     surface: wl_surface::WlSurface,
@@ -105,14 +114,9 @@ struct NotificationApp {
     device: wgpu::Device,
     queue: wgpu::Queue,
     config: wgpu::SurfaceConfiguration,
-    render_pipeline: wgpu::RenderPipeline,
     vertex_buffer: wgpu::Buffer,
     vertex_count: u32,
 
-    font_system: FontSystem,
-    swash_cache: SwashCache,
-    text_atlas: TextAtlas,
-    text_renderer: TextRenderer,
     text_viewport: Viewport,
 
     rects: Vec<RectWidget>,
@@ -122,6 +126,7 @@ struct NotificationApp {
     width: u32,
     height: u32,
     needs_rebuild: bool,
+    configured: bool,
 
     app_name: String,
     summary: String,
@@ -129,20 +134,27 @@ struct NotificationApp {
 }
 
 impl NotificationApp {
-    async fn new(
+    fn new(
         conn: &Connection,
         qh: &QueueHandle<AppState>,
         compositor_state: &CompositorState,
         xdg_shell_state: &XdgShell,
         width: u32,
         height: u32,
+        scale: f64,
+        instance: &wgpu::Instance,
+        adapter: &wgpu::Adapter,
+        device: wgpu::Device,
+        queue: wgpu::Queue,
+        cache: &Cache,
     ) -> Self {
         let surface = compositor_state.create_surface(qh);
+        surface.set_buffer_scale(scale as i32);
         let window = xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, qh);
         window.set_title("Notification");
-        window.set_app_id("clear-notifier");
-        window.set_min_size(Some((width, height)));
-        window.set_max_size(Some((width, height)));
+        window.set_app_id("clear-notification-daemon");
+        window.set_min_size(Some(((width as f64 / scale) as u32, (height as f64 / scale) as u32)));
+        window.set_max_size(Some(((width as f64 / scale) as u32, (height as f64 / scale) as u32)));
         window.commit();
 
         let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
@@ -150,75 +162,12 @@ impl NotificationApp {
             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("surface");
-        let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
-            power_preference: wgpu::PowerPreference::HighPerformance,
-            compatible_surface: Some(&wgpu_surface),
-            force_fallback_adapter: false,
-        }).await.expect("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("device");
-        let config = wgpu_surface.get_default_config(&adapter, width.max(1), height.max(1)).expect("config");
+        let mut config = wgpu_surface.get_default_config(adapter, width.max(1), height.max(1)).expect("config");
+        config.format = wgpu::TextureFormat::Bgra8Unorm;
         wgpu_surface.configure(&device, &config);
 
-        let shader_code = clear_ui::SHADER;
-        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Shader"),
-            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
-        });
-        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 font_system = FontSystem::new();
-        let swash_cache = SwashCache::new();
-        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);
+        let mut text_viewport = Viewport::new(&device, cache);
         text_viewport.update(&queue, Resolution { width, height });
 
         let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
@@ -228,7 +177,7 @@ impl NotificationApp {
             mapped_at_creation: false,
         });
 
-        let scale_factor = 2.0;
+        let scale_factor = scale;
 
         Self {
             window,
@@ -237,13 +186,8 @@ impl NotificationApp {
             device,
             queue,
             config,
-            render_pipeline,
             vertex_buffer,
             vertex_count: 0,
-            font_system,
-            swash_cache,
-            text_atlas,
-            text_renderer,
             text_viewport,
             rects: Vec::new(),
             text_items: Vec::new(),
@@ -251,13 +195,14 @@ impl NotificationApp {
             width,
             height,
             needs_rebuild: true,
+            configured: false,
             app_name: String::new(),
             summary: String::new(),
             body: String::new(),
         }
     }
 
-    fn rebuild_layout(&mut self) {
+    fn rebuild_layout(&mut self, font_system: &mut FontSystem) {
         let sw = self.width as f32;
         let sh = self.height as f32;
         let s = self.scale_factor as f32;
@@ -284,7 +229,7 @@ impl NotificationApp {
         });
 
         // 3. Text content
-        let app_name_buf = make_text_buffer(&mut self.font_system, &self.app_name, 10.0 * s);
+        let app_name_buf = make_text_buffer(font_system, &self.app_name, 10.0 * s);
         self.text_items.push(TextItem {
             buffer: app_name_buf,
             x: 18.0 * s,
@@ -296,7 +241,7 @@ impl NotificationApp {
             ),
         });
 
-        let summary_buf = make_text_buffer(&mut self.font_system, &self.summary, 13.0 * s);
+        let summary_buf = make_text_buffer(font_system, &self.summary, 13.0 * s);
         self.text_items.push(TextItem {
             buffer: summary_buf,
             x: 18.0 * s,
@@ -308,7 +253,7 @@ impl NotificationApp {
             ),
         });
 
-        let body_buf = make_text_buffer(&mut self.font_system, &self.body, 11.0 * s);
+        let body_buf = make_text_buffer(font_system, &self.body, 11.0 * s);
         self.text_items.push(TextItem {
             buffer: body_buf,
             x: 18.0 * s,
@@ -349,7 +294,13 @@ impl NotificationApp {
         self.queue.write_buffer(&self.vertex_buffer, 0, data);
     }
 
-    fn prepare_text(&mut self) {
+    fn prepare_text(
+        &mut self,
+        font_system: &mut FontSystem,
+        swash_cache: &mut SwashCache,
+        text_atlas: &mut TextAtlas,
+        text_renderer: &mut TextRenderer,
+    ) {
         let w = self.width as f32;
         let h = self.height as f32;
         let viewport = Resolution { width: w as u32, height: h as u32 };
@@ -361,9 +312,9 @@ impl NotificationApp {
             default_color: ti.color,
             custom_glyphs: &[],
         }).collect();
-        self.text_renderer.prepare(
-            &self.device, &self.queue, &mut self.font_system,
-            &mut self.text_atlas, &self.text_viewport, areas, &mut self.swash_cache
+        text_renderer.prepare(
+            &self.device, &self.queue, font_system,
+            text_atlas, &self.text_viewport, areas, swash_cache
         ).unwrap();
     }
 
@@ -378,12 +329,19 @@ impl NotificationApp {
         }
     }
 
-    fn render(&mut self) {
+    fn render(
+        &mut self,
+        render_pipeline: &wgpu::RenderPipeline,
+        font_system: &mut FontSystem,
+        swash_cache: &mut SwashCache,
+        text_atlas: &mut TextAtlas,
+        text_renderer: &mut TextRenderer,
+    ) {
         if self.needs_rebuild {
-            self.rebuild_layout();
+            self.rebuild_layout(font_system);
             self.upload_vertices();
         }
-        self.prepare_text();
+        self.prepare_text(font_system, swash_cache, text_atlas, text_renderer);
 
         let output = match self.wgpu_surface.get_current_texture() {
             Ok(t) => t,
@@ -416,11 +374,11 @@ impl NotificationApp {
                 occlusion_query_set: None,
             });
 
-            pass.set_pipeline(&self.render_pipeline);
+            pass.set_pipeline(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();
+            text_renderer.render(text_atlas, &self.text_viewport, &mut pass).unwrap();
         }
 
         self.queue.submit(std::iter::once(encoder.finish()));
@@ -451,12 +409,12 @@ fn play_bell_if_configured() {
     }
     
     if bell_enabled {
-        println!("[clear-notifier] Playing notification bell sound...");
+        println!("[clear-notification-daemon] Playing notification bell sound...");
         if let Err(e) = std::process::Command::new("pw-play")
             .arg("/usr/share/sounds/freedesktop/stereo/bell.oga")
             .spawn()
         {
-            eprintln!("[clear-notifier] Failed to spawn pw-play: {}", e);
+            eprintln!("[clear-notification-daemon] Failed to spawn pw-play: {}", e);
         }
     }
 }
@@ -521,6 +479,12 @@ struct AppState {
     qh: QueueHandle<AppState>,
     rt_handle: tokio::runtime::Handle,
     sender: calloop::channel::Sender<UserEvent>,
+
+    wgpu_instance: wgpu::Instance,
+    wgpu_adapter: wgpu::Adapter,
+    wgpu_device: wgpu::Device,
+    wgpu_queue: wgpu::Queue,
+    renderer_resources: RendererResources,
 }
 
 impl CompositorHandler for AppState {
@@ -531,9 +495,15 @@ impl CompositorHandler for AppState {
         _surface: &wl_surface::WlSurface,
         scale_factor: i32,
     ) {
+        _surface.set_buffer_scale(scale_factor);
         if let Some(state) = &mut self.state {
-            state.scale_factor = (scale_factor as f32).max(2.0) as f64;
-            state.resize(state.width, state.height);
+            let old_scale = state.scale_factor;
+            state.scale_factor = scale_factor as f64;
+            let logical_w = state.width as f64 / old_scale;
+            let logical_h = state.height as f64 / old_scale;
+            let pw = (logical_w * state.scale_factor) as u32;
+            let ph = (logical_h * state.scale_factor) as u32;
+            state.resize(pw, ph);
         }
         self.redraw = true;
     }
@@ -707,7 +677,10 @@ impl WindowHandler for AppState {
             let width = w.get();
             let height = h.get();
             if let Some(state) = &mut self.state {
-                state.resize(width, height);
+                state.configured = true;
+                let pw = (width as f64 * state.scale_factor) as u32;
+                let ph = (height as f64 * state.scale_factor) as u32;
+                state.resize(pw, ph);
             }
         }
         self.redraw = true;
@@ -761,22 +734,31 @@ impl AppState {
                 let active_id = self.current_id;
 
                 if self.state.is_none() {
-                    println!("[clear-notifier] Opening notification window: {} - {}", summary, body);
-                    let mut state = pollster::block_on(NotificationApp::new(
+                    println!("[clear-notification-daemon] Opening notification window: {} - {}", summary, body);
+                    let scale = clear_ui::wayland::detect_scale_factor(&self.output_state);
+                    let pw = (360.0 * scale) as u32;
+                    let ph = (100.0 * scale) as u32;
+                    let mut state = NotificationApp::new(
                         &self.conn,
                         &self.qh,
                         &self.compositor_state,
                         &self.xdg_shell_state,
-                        360,
-                        100,
-                    ));
+                        pw,
+                        ph,
+                        scale,
+                        &self.wgpu_instance,
+                        &self.wgpu_adapter,
+                        self.wgpu_device.clone(),
+                        self.wgpu_queue.clone(),
+                        &self.renderer_resources.cache,
+                    );
                     state.app_name = app_name;
                     state.summary = summary;
                     state.body = body;
                     state.needs_rebuild = true;
                     self.state = Some(state);
                 } else if let Some(ref mut state) = self.state {
-                    println!("[clear-notifier] Updating active notification window: {} - {}", summary, body);
+                    println!("[clear-notification-daemon] Updating active notification window: {} - {}", summary, body);
                     state.app_name = app_name;
                     state.summary = summary;
                     state.body = body;
@@ -794,7 +776,7 @@ impl AppState {
             }
             UserEvent::CloseNotification { notification_id } => {
                 if notification_id == self.current_id {
-                    println!("[clear-notifier] Closing notification window (ID: {})...", notification_id);
+                    println!("[clear-notification-daemon] Closing notification window (ID: {})...", notification_id);
                     self.state = None; // Dropping the window and resources
                     self.redraw = true;
                 }
@@ -843,7 +825,7 @@ impl DbusInterface {
 
     async fn get_server_information(&self) -> (String, String, String, String) {
         (
-            "clear-notifier".to_string(),
+            "clear-notification-daemon".to_string(),
             "ClearWM Project".to_string(),
             "0.1.0".to_string(),
             "1.2".to_string(),
@@ -864,6 +846,83 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
     let seat_state = SeatState::new(&globals, &qh);
     let output_state = OutputState::new(&globals, &qh);
 
+    // Initialize wgpu graphics context once on startup
+    let wgpu_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
+        backends: wgpu::Backends::VULKAN,
+        ..Default::default()
+    });
+    let wgpu_adapter = pollster::block_on(wgpu_instance.request_adapter(&wgpu::RequestAdapterOptions {
+        power_preference: wgpu::PowerPreference::HighPerformance,
+        compatible_surface: None,
+        force_fallback_adapter: false,
+    })).expect("Failed to find wgpu adapter");
+    let (wgpu_device, wgpu_queue) = pollster::block_on(wgpu_adapter.request_device(&wgpu::DeviceDescriptor {
+        label: Some("GPU Device"),
+        required_features: wgpu::Features::empty(),
+        required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(wgpu_adapter.limits()),
+        memory_hints: wgpu::MemoryHints::MemoryUsage,
+    }, None)).expect("Failed to request wgpu device");
+
+    // Initialize renderer resources once
+    let font_system = FontSystem::new();
+    let swash_cache = SwashCache::new();
+    let cache = Cache::new(&wgpu_device);
+    let mut text_atlas = TextAtlas::new(&wgpu_device, &wgpu_queue, &cache, wgpu::TextureFormat::Bgra8Unorm);
+    let text_renderer = TextRenderer::new(&mut text_atlas, &wgpu_device, wgpu::MultisampleState::default(), None);
+
+    let shader_code = clear_ui::SHADER;
+    let shader = wgpu_device.create_shader_module(wgpu::ShaderModuleDescriptor {
+        label: Some("Shader"),
+        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_code)),
+    });
+    let pipeline_layout = wgpu_device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+        label: Some("Pipeline Layout"),
+        bind_group_layouts: &[],
+        push_constant_ranges: &[],
+    });
+    let render_pipeline = wgpu_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: wgpu::TextureFormat::Bgra8Unorm,
+                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 renderer_resources = RendererResources {
+        render_pipeline,
+        font_system,
+        swash_cache,
+        text_atlas,
+        text_renderer,
+        cache,
+    };
+
     // Create the tokio runtime
     let rt = tokio::runtime::Builder::new_multi_thread()
         .enable_all()
@@ -891,7 +950,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                 .await
                 .expect("Failed to build D-Bus connection");
 
-            println!("[clear-notifier] D-Bus listener registered. Running...");
+            println!("[clear-notification-daemon] D-Bus listener registered. Running...");
             
             // Keep background runtime alive
             loop {
@@ -918,6 +977,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         qh,
         rt_handle: handle,
         sender,
+        wgpu_instance,
+        wgpu_adapter,
+        wgpu_device: wgpu_device.clone(),
+        wgpu_queue: wgpu_queue.clone(),
+        renderer_resources,
     };
 
     WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
@@ -928,7 +992,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         }
     }).unwrap();
 
-    println!("[clear-notifier] Wayland event loop starting...");
+    println!("[clear-notification-daemon] Wayland event loop starting...");
     loop {
         event_loop
             .dispatch(std::time::Duration::from_millis(16), &mut app)
@@ -939,7 +1003,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         if app.redraw {
             app.redraw = false;
             if let Some(state) = &mut app.state {
-                state.render();
+                if state.configured {
+                    let rr = &mut app.renderer_resources;
+                    state.render(
+                        &rr.render_pipeline,
+                        &mut rr.font_system,
+                        &mut rr.swash_cache,
+                        &mut rr.text_atlas,
+                        &mut rr.text_renderer,
+                    );
+                }
             }
         }
     }