Skip to main content

teksilo_core/window/
ops.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! App-level window-operation sink.
5//!
6//! [`WindowOps`] is the trait implemented by the app-level window
7//! manager (`teksilo_app::WindowManager`) and handed to every
8//! [`EventContext`](crate::widget::EventContext) during event
9//! dispatch. Handlers reach the multi-window API through it.
10//!
11//! Calls are **synchronous** except the two activation-token requests,
12//! which answer through a callback on a later event-loop tick, and a quit
13//! request, which the application carries out over the ticks that follow:
14//! `open_window` creates the winit window and registers it before
15//! returning; the returned id is immediately usable for
16//! `focus_window`, `window_state`, `close_window_by_id`. The trait exists to keep teksilo-core
17//! independent of teksilo-app — teksilo-core defines the contract, teksilo-app
18//! provides the implementation.
19
20use super::config::WindowConfig;
21use super::id::TeksiloWindowId;
22use super::state::WindowState;
23use crate::raw_handle::ParentHandle;
24
25/// App-level window operations exposed to handlers.
26///
27/// Implemented by `teksilo_app::WindowManager` (via a short-lived
28/// wrapper that also holds `&ActiveEventLoop`). Passed into every
29/// dispatch site as `&mut dyn WindowOps` and stored on
30/// [`EventContext`](crate::widget::EventContext).
31pub trait WindowOps {
32    /// Open a new window. Creates the winit-level window
33    /// synchronously inside this call and returns its id, which is
34    /// immediately valid for any other method on this trait.
35    fn open_window(&mut self, config: WindowConfig) -> TeksiloWindowId;
36
37    /// Look up a window by the stable string id it was opened with
38    /// (`WindowConfig::id`). Returns `None` if no live window carries
39    /// that id.
40    fn find_window(&self, string_id: &str) -> Option<TeksiloWindowId>;
41
42    /// Read the reactive state for a specific window.
43    fn window_state(&self, id: TeksiloWindowId) -> Option<WindowState>;
44
45    /// Every live window's state, in creation order.
46    fn windows(&self) -> Vec<WindowState>;
47
48    /// Raise a window and give it keyboard focus, restoring it first if it
49    /// is minimised.
50    fn focus_window(&mut self, id: TeksiloWindowId);
51
52    /// Request an `xdg_activation_v1` token for `id` — to hand to another window
53    /// or a child process so it can raise itself on Wayland. `cb` fires once with
54    /// the token string, or with `None` where unsupported (everything but
55    /// Wayland/X11). Default implementation: immediate `None`.
56    fn request_activation_token(
57        &mut self,
58        _id: TeksiloWindowId,
59        cb: Box<dyn FnOnce(Option<String>)>,
60    ) {
61        cb(None);
62    }
63
64    /// Like [`request_activation_token`](Self::request_activation_token) but for
65    /// the **current dispatching** window — the one whose handler is running.
66    /// Works even mid-dispatch, when that window is temporarily out of the
67    /// manager's map, because it uses the captured window handle instead of an id
68    /// lookup. Use this when a focused widget needs a token to hand to another
69    /// window or process. Default implementation: immediate `None`.
70    fn request_activation_token_self(&mut self, cb: Box<dyn FnOnce(Option<String>)>) {
71        cb(None);
72    }
73
74    /// Close a specific window by id. The window is fully torn down
75    /// before the next event-loop tick.
76    fn close_window_by_id(&mut self, id: TeksiloWindowId);
77
78    /// Extract the platform parent handle of the window currently
79    /// dispatching the event (the one that owns the in-flight
80    /// `EventContext`). Used by native-dialog integrations
81    /// (`teksilo_platform::file_dialog`) to parent OS dialogs to the
82    /// originating Teksilo window.
83    ///
84    /// Returns `None` for the standalone / test sink and on rare
85    /// platform paths where the underlying surface refuses a handle
86    /// (e.g. during shutdown).
87    fn current_parent_handle(&self) -> Option<ParentHandle> {
88        None
89    }
90
91    /// Report the focused text widget's caret rectangle (in window-logical
92    /// pixels) so the platform can position the OS IME candidate window
93    /// next to the insertion point. Called from text-editing widgets
94    /// whenever the caret moves. The app applies it to the in-flight
95    /// window's `set_ime_cursor_area`, deduped against the last value.
96    ///
97    /// No-op on the standalone / test sink.
98    fn set_ime_cursor_area(&mut self, _area: teksilo_canvas::Rect) {}
99
100    /// Hand an OS-level drag to the platform when an in-app drag escalates at
101    /// the window boundary (the pointer left the window carrying an
102    /// OS-exportable payload). The platform backend
103    /// (`teksilo_platform::external_dnd`) starts a native drag session
104    /// (`NSDraggingSource` / `wl_data_source` / OLE `IDropSource`) using
105    /// `data`, optionally drawing `image` as the drag cursor.
106    ///
107    /// Returns `true` if a native drag session actually started. The default
108    /// (standalone / test sink, and platforms without an outbound backend,
109    /// a headless build, or a target with no drop-target implementation)
110    /// returns `false`, in which case the framework cancels the drag
111    /// — the pre-existing "pointer left the window ⇒ drag cancels" behavior.
112    /// `pointer` is the device carrying the drag. It is not decoration: on
113    /// Wayland `wl_data_device::start_drag` must be given the serial of the
114    /// input event that began the implicit grab, and a finger's grab was opened
115    /// by a `wl_touch::down`, not a `wl_pointer::button` — hand the wrong
116    /// serial over and the compositor rejects the request silently and sends no
117    /// terminal event at all.
118    fn begin_os_drag(
119        &mut self,
120        _data: crate::drag_payload::OutboundDragData,
121        _image: Option<crate::drag_payload::DragImageData>,
122        _pointer: teksilo_tokens::PointerKind,
123    ) -> bool {
124        false
125    }
126
127    /// Tell the platform whether the widget under an **inbound** OS drag
128    /// accepts it, so the OS shows the right cursor and permits (or refuses)
129    /// the drop.
130    ///
131    /// An inbound backend must answer the drag source synchronously — XDND
132    /// requires an `XdndStatus` for every `XdndPosition`, and Wayland wants
133    /// `wl_data_offer::accept` + `set_actions` on the offer — which happens on
134    /// the backend's own thread, before the widget tree has seen the sample. So
135    /// the backend's first answer can only be about *format* compatibility;
136    /// this is how the widget's actual verdict gets back to the OS. Called by
137    /// the tree only when the answer changes.
138    ///
139    /// The negotiated *operation* follows from the bit: Copy when accepted,
140    /// none when refused. Copy is the only operation Teksilo advertises in
141    /// either direction, so there is nothing else for a widget to choose — see
142    /// `docs/drag-and-drop.md` §11.5.
143    ///
144    /// Default: no-op — the standalone sink and any platform without an
145    /// inbound backend.
146    fn set_drop_accepted(&mut self, _accepted: bool) {}
147
148    /// Abandon an OS drag started by [`Self::begin_os_drag`] (the user pressed
149    /// Escape).
150    ///
151    /// Only backends that drive the drag themselves can honour this. macOS and
152    /// Windows hand the drag to a modal OS loop that owns Escape already, and
153    /// Wayland's compositor does the same; X11 tracks the pointer on its own
154    /// connection, so without this its drags could only end by releasing the
155    /// button. The backend still reports the terminal `DropOutcome` either way,
156    /// so the source widget's `on_drag_ended` fires exactly once regardless.
157    ///
158    /// Default: no-op.
159    fn cancel_os_drag(&mut self) {}
160
161    /// What the host platform can do about an on-screen keyboard.
162    ///
163    /// Read by a widget that must decide whether a touch-only user can reach a
164    /// keyboard at all: where the answer is [`SoftKeyboardSupport::None`] the
165    /// framework will never raise one and promises nothing about whether the
166    /// platform will, so a text surface that expects a finger has to offer its
167    /// own affordance.
168    ///
169    /// Default: [`SoftKeyboardSupport::None`], which is the truth for a
170    /// standalone tree with no window under it.
171    fn soft_keyboard_support(&self) -> SoftKeyboardSupport {
172        SoftKeyboardSupport::None
173    }
174
175    /// Ask the application to quit, asking every open window's close guard
176    /// in turn. See [`EventContext::quit`](crate::widget::EventContext::quit).
177    ///
178    /// Default: no-op, for a sink with no application behind it.
179    fn quit(&mut self) {}
180
181    /// Quit the application without asking any close guard. See
182    /// [`EventContext::quit_forced`](crate::widget::EventContext::quit_forced).
183    ///
184    /// Default: no-op, for a sink with no application behind it.
185    fn quit_forced(&mut self) {}
186
187    /// Whether a quit is under way: requested, and neither finished nor
188    /// abandoned. Default: `false`.
189    fn quit_in_progress(&self) -> bool {
190        false
191    }
192
193    /// Where the quit under way came from, or `None` when no quit is under
194    /// way. Default: `None`.
195    fn quit_source(&self) -> Option<crate::window::QuitSource> {
196        None
197    }
198
199    /// Abandon the quit under way, if it is a guarded one; see
200    /// [`EventContext::cancel_quit`](crate::widget::EventContext::cancel_quit).
201    /// Default: no-op.
202    fn cancel_quit(&mut self) {}
203
204    /// Take the activation token the event being dispatched carried, while
205    /// no window operation has used it: an intent kept for another tree takes
206    /// it with it, and nothing else can use it afterwards (a token is good
207    /// for one activation). Default: `None`, for a sink with no application
208    /// behind it.
209    #[doc(hidden)]
210    fn take_activation_token(&mut self) -> Option<String> {
211        None
212    }
213
214    /// Hold `token`, which an intent routed from another tree carried, for
215    /// the first window the actions it reaches open or raise. Paired with
216    /// [`release_activation_token`](Self::release_activation_token).
217    /// Default: no-op.
218    #[doc(hidden)]
219    fn hold_activation_token(&mut self, _token: String) {}
220
221    /// The routed intent's actions have run: a token they did not use is
222    /// left to a `WindowState::focus()` they queued, for this turn only, as
223    /// after any event that carried one, unless a token already waits for
224    /// such a focus (an earlier handler of the turn left it): that one
225    /// stays, whether this one was used or not. Default: no-op.
226    #[doc(hidden)]
227    fn release_activation_token(&mut self) {}
228}
229
230/// What a platform can do about an on-screen keyboard.
231///
232/// Three answers, and the difference between them is what a caller may
233/// *promise a user*, not how much code stands behind them.
234#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Hash)]
235#[non_exhaustive]
236pub enum SoftKeyboardSupport {
237    /// The framework has no keyboard request to send, and makes no promise
238    /// that anything will rise on its own. Either the platform has no software
239    /// keyboard at all, or it has one whose appearance is the platform's
240    /// business and not reliable enough to promise. A request is dropped, and a
241    /// text surface driven by touch needs its own affordance.
242    #[default]
243    None,
244    /// A keyboard exists and is **guaranteed** to rise when a text control
245    /// takes focus through the accessibility layer, so a touch-driven text
246    /// surface needs no affordance of its own. There is still nothing to ask:
247    /// the framework's ordinary IME-allowance reconcile is what summons it, and
248    /// an explicit ask would at best duplicate that and at worst re-assert
249    /// allowance, which cancels a live composition — so a request resolves to
250    /// "already done".
251    ///
252    /// The guarantee is what separates this from [`None`](Self::None), which
253    /// covers every platform where a keyboard may or may not appear.
254    ViaAccessibility,
255    /// A keyboard exists and can be shown and hidden on demand. Only a backend
256    /// that can honour **both** directions may report this: a toggle whose
257    /// current state is unknown cannot, because "show" would sometimes hide.
258    Explicit,
259}
260
261/// No-op implementation used by standalone `WidgetTree`s constructed
262/// outside of an app (tests, headless scenarios). Every method
263/// returns `None` / does nothing; `open_window` panics because a
264/// standalone tree has no winit back-end to create windows in.
265pub struct NoopWindowOps;
266
267impl WindowOps for NoopWindowOps {
268    fn open_window(&mut self, _config: WindowConfig) -> TeksiloWindowId {
269        panic!("open_window called on a standalone WidgetTree (no app context)");
270    }
271
272    fn find_window(&self, _string_id: &str) -> Option<TeksiloWindowId> {
273        None
274    }
275
276    fn window_state(&self, _id: TeksiloWindowId) -> Option<WindowState> {
277        None
278    }
279
280    fn windows(&self) -> Vec<WindowState> {
281        Vec::new()
282    }
283
284    fn focus_window(&mut self, _id: TeksiloWindowId) {}
285
286    fn close_window_by_id(&mut self, _id: TeksiloWindowId) {}
287}