Skip to main content

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