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