Skip to main content

plushie_renderer_lib/
events.rs

1//! Subscription event handlers for keyboard, mouse, touch, IME, window
2//! lifecycle, and pane grid events. Each handler checks whether the host
3//! subscribed to the event type before emitting it.
4
5use std::io;
6
7use iced::{Point, Task, window};
8
9use plushie_widget_sdk::protocol::OutgoingEvent;
10use plushie_widget_sdk::protocol::OutgoingEventKeyExt;
11use plushie_widget_sdk::runtime::{
12    KeyEventData, Message, serialize_modifiers, serialize_mouse_button, serialize_scroll_delta,
13};
14
15use crate::App;
16use crate::constants::*;
17
18/// Convert a file path to a UTF-8 string, using lossy conversion if
19/// the path contains non-UTF-8 bytes (rare on modern systems, but
20/// possible on Linux with legacy filenames).
21fn path_to_string(path: std::path::PathBuf) -> String {
22    match path.to_str() {
23        Some(s) => s.to_string(),
24        None => {
25            log::warn!(
26                "file path contains non-UTF-8 bytes, using lossy conversion: {}",
27                path.display()
28            );
29            path.to_string_lossy().into_owned()
30        }
31    }
32}
33
34impl App {
35    /// Resolve an iced window::Id to a string window_id. Returns `None`
36    /// for unresolved windows (e.g., late events after a window close).
37    /// Callers should skip event emission when this returns `None`.
38    fn resolve_window_id(&self, iced_id: &window::Id) -> Option<&str> {
39        let resolved = self.windows.get_window_id(iced_id);
40        if resolved.is_none() {
41            log::debug!("event for unknown iced window {:?}, suppressing", iced_id);
42        }
43        resolved
44    }
45
46    pub fn handle_key_pressed(&self, data: KeyEventData, iced_id: window::Id) -> Task<Message> {
47        let Some(window_id) = self.resolve_window_id(&iced_id) else {
48            return Task::none();
49        };
50        self.emit_subscription_for_window(SUB_KEY_PRESS, Some(window_id), data.captured, |tag| {
51            OutgoingEvent::key_press(tag, &data)
52        })
53    }
54
55    pub fn handle_key_released(&self, data: KeyEventData, iced_id: window::Id) -> Task<Message> {
56        let Some(window_id) = self.resolve_window_id(&iced_id) else {
57            return Task::none();
58        };
59        self.emit_subscription_for_window(SUB_KEY_RELEASE, Some(window_id), data.captured, |tag| {
60            OutgoingEvent::key_release(tag, &data)
61        })
62    }
63
64    pub fn handle_modifiers_changed(
65        &mut self,
66        mods: iced::keyboard::Modifiers,
67        iced_id: window::Id,
68        captured: bool,
69    ) -> Task<Message> {
70        self.current_modifiers = mods;
71        let Some(window_id) = self.windows.get_window_id(&iced_id) else {
72            log::debug!("event for unknown iced window {:?}, suppressing", iced_id);
73            return Task::none();
74        };
75        crate::app::coalesce_subscription_into(
76            &self.core,
77            &mut self.emitter,
78            SUB_MODIFIERS_CHANGED,
79            Some(window_id),
80            captured,
81            |tag| OutgoingEvent::modifiers_changed(tag, serialize_modifiers(mods)),
82        )
83    }
84
85    pub fn handle_cursor_moved(
86        &mut self,
87        pos: Point,
88        iced_id: window::Id,
89        captured: bool,
90    ) -> Task<Message> {
91        let Some(window_id) = self.windows.get_window_id(&iced_id) else {
92            log::debug!("event for unknown iced window {:?}, suppressing", iced_id);
93            return Task::none();
94        };
95        crate::app::coalesce_subscription_into(
96            &self.core,
97            &mut self.emitter,
98            SUB_POINTER_MOVE,
99            Some(window_id),
100            captured,
101            |tag| OutgoingEvent::cursor_moved(tag, pos.x, pos.y),
102        )
103    }
104
105    pub fn handle_cursor_entered(&self, iced_id: window::Id, captured: bool) -> Task<Message> {
106        let Some(window_id) = self.resolve_window_id(&iced_id) else {
107            return Task::none();
108        };
109        self.emit_subscription_for_window(SUB_POINTER_MOVE, Some(window_id), captured, |tag| {
110            OutgoingEvent::cursor_entered(tag)
111        })
112    }
113
114    pub fn handle_cursor_left(&self, iced_id: window::Id, captured: bool) -> Task<Message> {
115        let Some(window_id) = self.resolve_window_id(&iced_id) else {
116            return Task::none();
117        };
118        self.emit_subscription_for_window(SUB_POINTER_MOVE, Some(window_id), captured, |tag| {
119            OutgoingEvent::cursor_left(tag)
120        })
121    }
122
123    pub fn handle_mouse_button_pressed(
124        &self,
125        button: iced::mouse::Button,
126        iced_id: window::Id,
127        captured: bool,
128    ) -> Task<Message> {
129        let Some(window_id) = self.resolve_window_id(&iced_id) else {
130            return Task::none();
131        };
132        self.emit_subscription_for_window(SUB_POINTER_BUTTON, Some(window_id), captured, |tag| {
133            OutgoingEvent::button_pressed(tag, serialize_mouse_button(&button))
134        })
135    }
136
137    pub fn handle_mouse_button_released(
138        &self,
139        button: iced::mouse::Button,
140        iced_id: window::Id,
141        captured: bool,
142    ) -> Task<Message> {
143        let Some(window_id) = self.resolve_window_id(&iced_id) else {
144            return Task::none();
145        };
146        self.emit_subscription_for_window(SUB_POINTER_BUTTON, Some(window_id), captured, |tag| {
147            OutgoingEvent::button_released(tag, serialize_mouse_button(&button))
148        })
149    }
150
151    pub fn handle_wheel_scrolled(
152        &mut self,
153        delta: iced::mouse::ScrollDelta,
154        iced_id: window::Id,
155        captured: bool,
156    ) -> Task<Message> {
157        let Some(window_id) = self.windows.get_window_id(&iced_id) else {
158            log::debug!("event for unknown iced window {:?}, suppressing", iced_id);
159            return Task::none();
160        };
161        crate::app::coalesce_subscription_into(
162            &self.core,
163            &mut self.emitter,
164            SUB_POINTER_SCROLL,
165            Some(window_id),
166            captured,
167            |tag| {
168                let (dx, dy, unit) = serialize_scroll_delta(&delta);
169                OutgoingEvent::wheel_scrolled(tag, dx, dy, unit)
170            },
171        )
172    }
173
174    pub fn handle_finger_pressed(
175        &self,
176        finger: iced::touch::Finger,
177        pos: Point,
178        iced_id: window::Id,
179        captured: bool,
180    ) -> Task<Message> {
181        let Some(window_id) = self.resolve_window_id(&iced_id) else {
182            return Task::none();
183        };
184        self.emit_subscription_for_window(SUB_POINTER_TOUCH, Some(window_id), captured, |tag| {
185            OutgoingEvent::finger_pressed(tag, finger.0, pos.x, pos.y)
186        })
187    }
188
189    pub fn handle_finger_moved(
190        &mut self,
191        finger: iced::touch::Finger,
192        pos: Point,
193        iced_id: window::Id,
194        captured: bool,
195    ) -> Task<Message> {
196        let Some(window_id) = self.windows.get_window_id(&iced_id) else {
197            log::debug!("event for unknown iced window {:?}, suppressing", iced_id);
198            return Task::none();
199        };
200        crate::app::coalesce_subscription_into(
201            &self.core,
202            &mut self.emitter,
203            SUB_POINTER_TOUCH,
204            Some(window_id),
205            captured,
206            |tag| OutgoingEvent::finger_moved(tag, finger.0, pos.x, pos.y),
207        )
208    }
209
210    pub fn handle_finger_lifted(
211        &self,
212        finger: iced::touch::Finger,
213        pos: Point,
214        iced_id: window::Id,
215        captured: bool,
216    ) -> Task<Message> {
217        let Some(window_id) = self.resolve_window_id(&iced_id) else {
218            return Task::none();
219        };
220        self.emit_subscription_for_window(SUB_POINTER_TOUCH, Some(window_id), captured, |tag| {
221            OutgoingEvent::finger_lifted(tag, finger.0, pos.x, pos.y)
222        })
223    }
224
225    pub fn handle_finger_lost(
226        &self,
227        finger: iced::touch::Finger,
228        pos: Point,
229        iced_id: window::Id,
230        captured: bool,
231    ) -> Task<Message> {
232        let Some(window_id) = self.resolve_window_id(&iced_id) else {
233            return Task::none();
234        };
235        self.emit_subscription_for_window(SUB_POINTER_TOUCH, Some(window_id), captured, |tag| {
236            OutgoingEvent::finger_lost(tag, finger.0, pos.x, pos.y)
237        })
238    }
239
240    // IME (Input Method Editor) events for CJK and complex input.
241    //
242    // Platform support: Windows (Microsoft IME, Google Japanese, etc.),
243    // macOS (built-in input methods), Linux/X11 (XIM/IBus), Linux/Wayland
244    // (text-input-v3 protocol; compositor support varies). The preedit
245    // cursor range may be None on some older X11 IME implementations.
246    pub fn handle_ime_opened(&self, iced_id: window::Id, captured: bool) -> Task<Message> {
247        let Some(window_id) = self.resolve_window_id(&iced_id) else {
248            return Task::none();
249        };
250        self.emit_subscription_for_window(SUB_IME, Some(window_id), captured, |tag| {
251            OutgoingEvent::ime_opened(tag)
252        })
253    }
254
255    pub fn handle_ime_preedit(
256        &self,
257        text: String,
258        cursor: Option<std::ops::Range<usize>>,
259        iced_id: window::Id,
260        captured: bool,
261    ) -> Task<Message> {
262        let Some(window_id) = self.resolve_window_id(&iced_id) else {
263            return Task::none();
264        };
265        self.emit_subscription_for_window(SUB_IME, Some(window_id), captured, |tag| {
266            OutgoingEvent::ime_preedit(tag, text.as_str(), cursor.clone())
267        })
268    }
269
270    pub fn handle_ime_commit(
271        &self,
272        text: String,
273        iced_id: window::Id,
274        captured: bool,
275    ) -> Task<Message> {
276        let Some(window_id) = self.resolve_window_id(&iced_id) else {
277            return Task::none();
278        };
279        self.emit_subscription_for_window(SUB_IME, Some(window_id), captured, |tag| {
280            OutgoingEvent::ime_commit(tag, text.as_str())
281        })
282    }
283
284    pub fn handle_ime_closed(&self, iced_id: window::Id, captured: bool) -> Task<Message> {
285        let Some(window_id) = self.resolve_window_id(&iced_id) else {
286            return Task::none();
287        };
288        self.emit_subscription_for_window(SUB_IME, Some(window_id), captured, |tag| {
289            OutgoingEvent::ime_closed(tag)
290        })
291    }
292
293    /// Emit a window event to all matching entries across the catch-all
294    /// window subscription and the event-specific subscription (if registered),
295    /// filtered by window_id scope.
296    ///
297    /// The closure receives `(tag, window_id)` as `&str` slices; the
298    /// event constructor allocates the owned strings internally via
299    /// `impl Into<String>`. Earlier versions cloned both at the call
300    /// site before passing them in.
301    fn emit_window_event(
302        &self,
303        specific_key: Option<&str>,
304        event_fn: impl Fn(&str, &str) -> OutgoingEvent,
305        window_id: String,
306    ) -> io::Result<()> {
307        let wid = Some(window_id.as_str());
308        // Emit for catch-all SUB_WINDOW_EVENT entries
309        for entry in self.core.matching_entries(SUB_WINDOW_EVENT, wid) {
310            self.emitter
311                .emit_event(event_fn(entry.tag.as_str(), window_id.as_str()))?;
312        }
313        // Emit for specific key entries (e.g. SUB_WINDOW_MOVE)
314        if let Some(key) = specific_key {
315            for entry in self.core.matching_entries(key, wid) {
316                self.emitter
317                    .emit_event(event_fn(entry.tag.as_str(), window_id.as_str()))?;
318            }
319        }
320        Ok(())
321    }
322
323    pub fn handle_window_event(&self, iced_id: window::Id, evt: window::Event) -> Task<Message> {
324        let Some(window_id) = self.resolve_window_id(&iced_id).map(str::to_string) else {
325            return Task::none();
326        };
327        // Helper closure: emit and propagate errors uniformly.
328        let result: io::Result<()> = (|| {
329            match evt {
330                window::Event::Opened {
331                    position,
332                    size,
333                    scale_factor,
334                } => {
335                    let wid = Some(window_id.as_str());
336                    let pos = position.map(|p| (p.x, p.y));
337                    for entry in self.core.matching_entries(SUB_WINDOW_EVENT, wid) {
338                        self.emitter.emit_event(OutgoingEvent::window_opened(
339                            entry.tag.as_str(),
340                            window_id.as_str(),
341                            pos,
342                            size.width,
343                            size.height,
344                            scale_factor,
345                        ))?;
346                    }
347                    for entry in self.core.matching_entries(SUB_WINDOW_OPEN, wid) {
348                        self.emitter.emit_event(OutgoingEvent::window_opened(
349                            entry.tag.as_str(),
350                            window_id.as_str(),
351                            pos,
352                            size.width,
353                            size.height,
354                            scale_factor,
355                        ))?;
356                    }
357                }
358                window::Event::Closed => {
359                    let wid = Some(window_id.as_str());
360                    for entry in self.core.matching_entries(SUB_WINDOW_EVENT, wid) {
361                        self.emitter.emit_event(OutgoingEvent::window_closed(
362                            entry.tag.as_str(),
363                            window_id.as_str(),
364                        ))?;
365                    }
366                }
367                window::Event::Moved(point) => {
368                    self.emit_window_event(
369                        Some(SUB_WINDOW_MOVE),
370                        |tag, jid| OutgoingEvent::window_moved(tag, jid, point.x, point.y),
371                        window_id,
372                    )?;
373                }
374                window::Event::Resized(size) => {
375                    self.emit_window_event(
376                        Some(SUB_WINDOW_RESIZE),
377                        |tag, jid| OutgoingEvent::window_resized(tag, jid, size.width, size.height),
378                        window_id,
379                    )?;
380                }
381                window::Event::Rescaled(factor) => {
382                    let wid = Some(window_id.as_str());
383                    for entry in self.core.matching_entries(SUB_WINDOW_EVENT, wid) {
384                        self.emitter.emit_event(OutgoingEvent::window_rescaled(
385                            entry.tag.as_str(),
386                            window_id.as_str(),
387                            factor,
388                        ))?;
389                    }
390                }
391                window::Event::Focused => {
392                    self.emit_window_event(
393                        Some(SUB_WINDOW_FOCUS),
394                        |tag, jid| OutgoingEvent::window_focused(tag, jid),
395                        window_id,
396                    )?;
397                }
398                window::Event::Unfocused => {
399                    self.emit_window_event(
400                        Some(SUB_WINDOW_UNFOCUS),
401                        |tag, jid| OutgoingEvent::window_unfocused(tag, jid),
402                        window_id,
403                    )?;
404                }
405                window::Event::FileHovered(path) => {
406                    let wid = Some(window_id.as_str());
407                    let path_str = path_to_string(path);
408                    for entry in self.core.matching_entries(SUB_FILE_DROP, wid) {
409                        self.emitter.emit_event(OutgoingEvent::file_hovered(
410                            entry.tag.as_str(),
411                            window_id.as_str(),
412                            path_str.as_str(),
413                        ))?;
414                    }
415                }
416                window::Event::FileDropped(path) => {
417                    let wid = Some(window_id.as_str());
418                    let path_str = path_to_string(path);
419                    for entry in self.core.matching_entries(SUB_FILE_DROP, wid) {
420                        self.emitter.emit_event(OutgoingEvent::file_dropped(
421                            entry.tag.as_str(),
422                            window_id.as_str(),
423                            path_str.as_str(),
424                        ))?;
425                    }
426                }
427                window::Event::FilesHoveredLeft => {
428                    let wid = Some(window_id.as_str());
429                    for entry in self.core.matching_entries(SUB_FILE_DROP, wid) {
430                        self.emitter.emit_event(OutgoingEvent::files_hovered_left(
431                            entry.tag.as_str(),
432                            window_id.as_str(),
433                        ))?;
434                    }
435                }
436                window::Event::CloseRequested => {
437                    // Handled via close_requests() subscription separately.
438                }
439                window::Event::RedrawRequested(_) => {
440                    // Handled via animation_frame subscription separately.
441                }
442            }
443            Ok(())
444        })();
445        if let Err(e) = result {
446            log::error!("write error: {e}");
447            return iced::exit();
448        }
449        Task::none()
450    }
451}