kas_core/event/cx/
window.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Event state: window management
7
8use super::{EventCx, EventState, PopupState};
9use crate::cast::Cast;
10use crate::event::{Event, FocusSource};
11use crate::runner::{AppData, Platform, RunnerT, WindowDataErased};
12#[cfg(all(wayland_platform, feature = "clipboard"))]
13use crate::util::warn_about_error;
14use crate::window::{PopupDescriptor, Window, WindowId, WindowWidget};
15use crate::{Action, Id, Node, Tile};
16use winit::window::ResizeDirection;
17
18impl EventState {
19    /// Get the platform
20    pub fn platform(&self) -> Platform {
21        self.platform
22    }
23
24    /// True when the window has focus
25    #[inline]
26    pub fn window_has_focus(&self) -> bool {
27        self.window_has_focus
28    }
29
30    // Remove popup at index and return its [`WindowId`]
31    //
32    // Panics if `index` is out of bounds.
33    //
34    // The caller must call `runner.close_window(window_id)`.
35    #[must_use]
36    pub(super) fn close_popup(&mut self, index: usize) -> WindowId {
37        let state = self.popups.remove(index);
38        if state.is_sized {
39            self.popup_removed.push((state.desc.id, state.id));
40        }
41        self.mouse.tooltip_popup_close(&state.desc.parent);
42
43        if let Some(id) = state.old_nav_focus {
44            self.set_nav_focus(id, FocusSource::Synthetic);
45        }
46
47        state.id
48    }
49
50    pub(crate) fn confirm_popup_is_sized(&mut self, id: WindowId) {
51        for popup in &mut self.popups {
52            if popup.id == id {
53                popup.is_sized = true;
54            }
55        }
56    }
57
58    /// Handle all pending items before event loop sleeps
59    pub(crate) fn flush_pending<'a>(
60        &'a mut self,
61        runner: &'a mut dyn RunnerT,
62        window: &'a dyn WindowDataErased,
63        mut node: Node,
64    ) -> Action {
65        if !self.pending_send_targets.is_empty() {
66            runner.set_send_targets(&mut self.pending_send_targets);
67        }
68
69        self.with(runner, window, |cx| {
70            while let Some((id, wid)) = cx.popup_removed.pop() {
71                cx.send_event(node.re(), id, Event::PopupClosed(wid));
72            }
73
74            cx.mouse_handle_pending(node.re());
75            cx.touch_handle_pending(node.re());
76
77            if let Some(id) = cx.pending_update.take() {
78                node.find_node(&id, |node| cx.update(node));
79            }
80
81            if cx.pending_nav_focus.is_some() {
82                cx.handle_pending_nav_focus(node.re());
83            }
84
85            // Update sel focus after nav focus:
86            if let Some(pending) = cx.pending_sel_focus.take() {
87                cx.set_sel_focus(cx.window, node.re(), pending);
88            }
89
90            // Poll futures; these may push messages to cx.send_queue.
91            cx.poll_futures();
92
93            let window_id = Id::ROOT.make_child(cx.window_id.get().cast());
94            while let Some((mut id, msg)) = cx.send_queue.pop_front() {
95                if !id.is_valid() {
96                    id = match cx.runner.send_target_for(msg.type_id()) {
97                        Some(target) => target,
98                        None => {
99                            // Perhaps ConfigCx::set_send_target_for should have been called?
100                            log::warn!(target: "kas_core::erased", "no send target for: {msg:?}");
101                            continue;
102                        }
103                    }
104                }
105
106                if window_id.is_ancestor_of(&id) {
107                    cx.send_or_replay(node.re(), id, msg);
108                } else {
109                    cx.runner.send_erased(id, msg);
110                }
111            }
112
113            // Finally, clear the region_moved flag (mouse and touch sub-systems handle this).
114            if cx.action.contains(Action::REGION_MOVED) {
115                cx.action.remove(Action::REGION_MOVED);
116                cx.action.insert(Action::REDRAW);
117            }
118        });
119
120        if let Some(icon) = self.mouse.update_cursor_icon() {
121            window.set_cursor_icon(icon);
122        }
123
124        std::mem::take(&mut self.action)
125    }
126
127    /// Window has been closed: clean up state
128    pub(crate) fn suspended(&mut self, runner: &mut dyn RunnerT) {
129        while !self.popups.is_empty() {
130            let id = self.close_popup(self.popups.len() - 1);
131            runner.close_window(id);
132        }
133    }
134}
135
136impl<'a> EventCx<'a> {
137    // Closes any popup which is not an ancestor of `id`
138    pub(super) fn close_non_ancestors_of(&mut self, id: Option<&Id>) {
139        for index in (0..self.popups.len()).rev() {
140            if let Some(id) = id
141                && self.popups[index].desc.id.is_ancestor_of(id)
142            {
143                continue;
144            }
145
146            let id = self.close_popup(index);
147            self.runner.close_window(id);
148        }
149    }
150
151    pub(super) fn handle_close(&mut self) {
152        let mut id = self.window_id;
153        if !self.popups.is_empty() {
154            let index = self.popups.len() - 1;
155            id = self.close_popup(index);
156        }
157        self.runner.close_window(id);
158    }
159
160    /// Add a pop-up
161    ///
162    /// A pop-up is a box used for things like tool-tips and menus which is
163    /// drawn on top of other content and has focus for input.
164    ///
165    /// Depending on the host environment, the pop-up may be a special type of
166    /// window without borders and with precise placement, or may be a layer
167    /// drawn in an existing window.
168    ///
169    /// The popup automatically receives mouse-motion events
170    /// ([`Event::CursorMove`]) which may be used to navigate menus.
171    /// The parent automatically receives the "depressed" visual state.
172    ///
173    /// It is recommended to call [`EventState::set_nav_focus`] or
174    /// [`EventState::next_nav_focus`] after this method.
175    ///
176    /// A pop-up may be closed by calling [`EventCx::close_window`] with
177    /// the [`WindowId`] returned by this method.
178    pub(crate) fn add_popup(&mut self, popup: PopupDescriptor, set_focus: bool) -> WindowId {
179        log::trace!(target: "kas_core::event", "add_popup: {popup:?}");
180
181        let parent_id = self.window.window_id();
182        let id = self.runner.add_popup(parent_id, popup.clone());
183        let mut old_nav_focus = None;
184        if set_focus {
185            old_nav_focus = self.nav_focus.clone();
186            self.clear_nav_focus();
187        }
188        self.popups.push(PopupState {
189            id,
190            desc: popup,
191            old_nav_focus,
192            is_sized: false,
193        });
194        id
195    }
196
197    /// Resize and reposition an existing pop-up
198    ///
199    /// This method takes a new [`PopupDescriptor`]. Its first field, `id`, is
200    /// expected to remain unchanged but other fields may differ.
201    pub(crate) fn reposition_popup(&mut self, id: WindowId, desc: PopupDescriptor) {
202        self.runner.reposition_popup(id, desc.clone());
203        for popup in self.popups.iter_mut() {
204            if popup.id == id {
205                debug_assert_eq!(popup.desc.id, desc.id);
206                popup.desc = desc;
207                break;
208            }
209        }
210    }
211
212    /// Add a data-less window
213    ///
214    /// This method may be used to attach a new window to the UI at run-time.
215    /// This method supports windows which do not take data; see also
216    /// [`Self::add_window`].
217    ///
218    /// Adding the window is infallible. Opening the new window is theoretically
219    /// fallible (unlikely assuming a window has already been opened).
220    ///
221    /// If `modal`, then the new `window` is considered owned by this window
222    /// (the window the calling widget belongs to), preventing interaction with
223    /// this window until the new `window` has been closed. **Note:** this is
224    /// mostly unimplemented; see [`Window::set_modal_with_parent`].
225    #[inline]
226    pub fn add_dataless_window(&mut self, mut window: Window<()>, modal: bool) -> WindowId {
227        if modal {
228            window.set_modal_with_parent(self.window_id);
229        }
230        self.runner.add_dataless_window(window)
231    }
232
233    /// Add a window able to access top-level app data
234    ///
235    /// This method may be used to attach a new window to the UI at run-time.
236    /// See also [`Self::add_dataless_window`] for a variant which does not
237    /// require a `Data` parameter.
238    ///
239    /// Requirement: the type `Data` must match the type of data passed to the
240    /// [`Runner`](https://docs.rs/kas/latest/kas/runner/struct.Runner.html)
241    /// and used by other windows. If not, a run-time error will result.
242    ///
243    /// Adding the window is infallible. Opening the new window is theoretically
244    /// fallible (unlikely assuming a window has already been opened).
245    #[inline]
246    pub fn add_window<Data: AppData>(&mut self, window: Window<Data>) -> WindowId {
247        let data_type_id = std::any::TypeId::of::<Data>();
248        unsafe {
249            let window: Window<()> = std::mem::transmute(window);
250            self.runner.add_window(window, data_type_id)
251        }
252    }
253
254    /// Close a window or pop-up
255    ///
256    /// Navigation focus will return to whichever widget had focus before
257    /// the popup was open.
258    pub fn close_window(&mut self, mut id: WindowId) {
259        for (index, p) in self.popups.iter().enumerate() {
260            if p.id == id {
261                id = self.close_popup(index);
262                break;
263            }
264        }
265
266        self.runner.close_window(id);
267    }
268
269    /// Enable window dragging for current click
270    ///
271    /// This calls [`winit::window::Window::drag_window`](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.drag_window). Errors are ignored.
272    pub fn drag_window(&self) {
273        if let Some(ww) = self.window.winit_window()
274            && let Err(e) = ww.drag_window()
275        {
276            log::warn!("EventCx::drag_window: {e}");
277        }
278    }
279
280    /// Enable window resizing for the current click
281    ///
282    /// This calls [`winit::window::Window::drag_resize_window`](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.drag_resize_window). Errors are ignored.
283    pub fn drag_resize_window(&self, direction: ResizeDirection) {
284        if let Some(ww) = self.window.winit_window()
285            && let Err(e) = ww.drag_resize_window(direction)
286        {
287            log::warn!("EventCx::drag_resize_window: {e}");
288        }
289    }
290
291    /// Attempt to get clipboard contents
292    ///
293    /// In case of failure, paste actions will simply fail. The implementation
294    /// may wish to log an appropriate warning message.
295    pub fn get_clipboard(&mut self) -> Option<String> {
296        #[cfg(all(wayland_platform, feature = "clipboard"))]
297        if let Some(cb) = self.window.wayland_clipboard() {
298            return match cb.load() {
299                Ok(s) => Some(s),
300                Err(e) => {
301                    warn_about_error("Failed to get clipboard contents", &e);
302                    None
303                }
304            };
305        }
306
307        self.runner.get_clipboard()
308    }
309
310    /// Attempt to set clipboard contents
311    pub fn set_clipboard(&mut self, content: String) {
312        #[cfg(all(wayland_platform, feature = "clipboard"))]
313        if let Some(cb) = self.window.wayland_clipboard() {
314            cb.store(content);
315            return;
316        }
317
318        self.runner.set_clipboard(content)
319    }
320
321    /// True if the primary buffer is enabled
322    #[inline]
323    pub fn has_primary(&self) -> bool {
324        cfg_if::cfg_if! {
325            if #[cfg(unix)] {
326                true
327            } else {
328                false
329            }
330        }
331    }
332
333    /// Get contents of primary buffer
334    ///
335    /// Linux has a "primary buffer" with implicit copy on text selection and
336    /// paste on middle-click. This method does nothing on other platforms.
337    pub fn get_primary(&mut self) -> Option<String> {
338        #[cfg(all(wayland_platform, feature = "clipboard"))]
339        if let Some(cb) = self.window.wayland_clipboard() {
340            return match cb.load_primary() {
341                Ok(s) => Some(s),
342                Err(e) => {
343                    warn_about_error("Failed to get clipboard contents", &e);
344                    None
345                }
346            };
347        }
348
349        self.runner.get_primary()
350    }
351
352    /// Set contents of primary buffer
353    ///
354    /// Linux has a "primary buffer" with implicit copy on text selection and
355    /// paste on middle-click. This method does nothing on other platforms.
356    pub fn set_primary(&mut self, content: String) {
357        #[cfg(all(wayland_platform, feature = "clipboard"))]
358        if let Some(cb) = self.window.wayland_clipboard() {
359            cb.store_primary(content);
360            return;
361        }
362
363        self.runner.set_primary(content)
364    }
365
366    /// Directly access Winit Window
367    ///
368    /// This is a temporary API, allowing e.g. to minimize the window.
369    pub fn winit_window(&self) -> Option<&winit::window::Window> {
370        self.window.winit_window()
371    }
372
373    /// Handle a winit `WindowEvent`.
374    ///
375    /// Note that some event types are not handled, since for these
376    /// events the graphics backend must take direct action anyway:
377    /// `Resized(size)`, `RedrawRequested`, `HiDpiFactorChanged(factor)`.
378    pub(crate) fn handle_winit<A>(
379        &mut self,
380        win: &mut dyn WindowWidget<Data = A>,
381        data: &A,
382        event: winit::event::WindowEvent,
383    ) {
384        use winit::event::WindowEvent::*;
385
386        match event {
387            CloseRequested => self.action(win.id(), Action::CLOSE),
388            /* Not yet supported: see #98
389            DroppedFile(path) => ,
390            HoveredFile(path) => ,
391            HoveredFileCancelled => ,
392            */
393            Focused(state) => {
394                self.window_has_focus = state;
395                if state {
396                    // Required to restart theme animations
397                    self.redraw(win.id());
398                } else {
399                    // Window focus lost: close all popups
400                    while let Some(id) = self.popups.last().map(|state| state.id) {
401                        self.close_window(id);
402                    }
403                }
404            }
405            KeyboardInput {
406                event,
407                is_synthetic,
408                ..
409            } => self.keyboard_input(win.as_node(data), event, is_synthetic),
410            ModifiersChanged(modifiers) => self.modifiers_changed(modifiers.state()),
411            Ime(event) => self.ime_event(win.as_node(data), event),
412            CursorMoved { position, .. } => self.handle_cursor_moved(win, data, position.into()),
413            CursorEntered { .. } => self.handle_cursor_entered(),
414            CursorLeft { .. } => self.handle_cursor_left(win.as_node(data)),
415            MouseWheel { delta, .. } => self.handle_mouse_wheel(win.as_node(data), delta),
416            MouseInput { state, button, .. } => {
417                self.handle_mouse_input(win.as_node(data), state, button)
418            }
419            // TouchpadPressure { pressure: f32, stage: i64, },
420            // AxisMotion { axis: AxisId, value: f64, },
421            Touch(touch) => self.handle_touch_event(win.as_node(data), touch),
422            _ => (),
423        }
424    }
425}