Skip to main content

incular_runtime/
window_commands.rs

1use crate::application_types::{WindowError, WindowRestorationId};
2use crate::context::BuildContext;
3use crate::file_dialogs::FileDialogService;
4use crate::request_channel::{ChannelError, RequestChannel, RequestReceiver};
5use crate::tasks::RuntimeWake;
6use crate::transient_presentation::TransientPresentationResolution;
7use crate::window_state::WindowManager;
8use incular_platform::{
9    CapabilitySupport, CursorGrabMode, DisplayPlacementArea, DisplaySnapshot, Fullscreen,
10    LogicalSizeLimits, LogicalWindowPosition, NativeRequestId, PhysicalScreenPosition,
11    PlatformCapabilities, PlatformOperationError, PlatformOperationResult, UserAttentionType,
12    WindowCommand, WindowIcon, WindowId, WindowLevel, WindowObservedState, WindowOperation,
13    WindowOptions,
14};
15use incular_widgets::Widget;
16use std::{
17    collections::BTreeMap,
18    future::Future,
19    pin::Pin,
20    sync::{
21        Arc, RwLock,
22        atomic::{AtomicBool, Ordering},
23        mpsc,
24    },
25    task::{Context, Poll},
26};
27use tokio::sync::oneshot;
28
29/// A portable UI-thread command emitted by the application for a native
30/// desktop adapter. It contains Incular IDs and options only—never Winit
31/// IDs, native pointers, or a GPU surface.
32#[derive(Clone, Debug, PartialEq)]
33pub enum NativeWindowCommand {
34    Create {
35        window_id: WindowId,
36        options: WindowOptions,
37    },
38    Operate(WindowCommand),
39}
40
41/// Failure to enqueue a window command into the runtime/UI bridge.
42///
43/// This says nothing about native execution. Operations that need a native
44/// result return a [`NativeOperationRequest`] after successful enqueue.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
46pub enum WindowCommandEnqueueError {
47    #[error("the Incular runtime command bridge has stopped")]
48    RuntimeStopped,
49    #[error("the native operation request-id space is exhausted")]
50    RequestIdExhausted,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
54pub enum WindowPlacementError {
55    #[error("top-level window placement is unsupported")]
56    Unsupported,
57    #[error("native outer window size is not available yet")]
58    OuterSizeUnavailable,
59    #[error("display {0:?} geometry is unavailable")]
60    DisplayGeometryUnavailable(DisplayPlacementArea),
61    #[error("{0}")]
62    Enqueue(WindowCommandEnqueueError),
63}
64
65#[derive(Default)]
66pub(crate) struct DisplayCatalog {
67    displays: BTreeMap<incular_platform::DisplayId, DisplaySnapshot>,
68    primary: Option<incular_platform::DisplayId>,
69}
70
71impl DisplayCatalog {
72    pub(crate) fn replace(
73        &mut self,
74        displays: impl IntoIterator<Item = DisplaySnapshot>,
75        primary: Option<incular_platform::DisplayId>,
76    ) {
77        self.displays = displays
78            .into_iter()
79            .map(|display| (display.id, display))
80            .collect();
81        self.primary = primary.filter(|id| self.displays.contains_key(id));
82    }
83
84    pub(crate) fn all(&self) -> Vec<DisplaySnapshot> {
85        self.displays.values().cloned().collect()
86    }
87
88    pub(crate) fn get(&self, id: incular_platform::DisplayId) -> Option<DisplaySnapshot> {
89        self.displays.get(&id).cloned()
90    }
91
92    pub(crate) fn primary(&self) -> Option<DisplaySnapshot> {
93        self.primary.and_then(|id| self.get(id))
94    }
95}
96
97impl From<WindowCommandEnqueueError> for WindowPlacementError {
98    fn from(value: WindowCommandEnqueueError) -> Self {
99        Self::Enqueue(value)
100    }
101}
102
103/// Runtime disposition after a backend reports one native completion.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum NativeOperationCompletionStatus {
106    Completed,
107    UnknownRequest,
108    TargetMismatch,
109    StaleTarget,
110}
111
112/// Awaitable result of one native platform operation.
113///
114/// Dropping the request only detaches result delivery; it does not attempt to
115/// undo a native operation that may already have executed. The runtime removes
116/// the pending completion registration on the next UI turn.
117#[must_use = "native operation requests must be awaited, inspected, or explicitly dropped"]
118pub struct NativeOperationRequest {
119    id: NativeRequestId,
120    receiver: RequestReceiver<PlatformOperationResult>,
121}
122
123impl NativeOperationRequest {
124    #[must_use]
125    pub const fn id(&self) -> NativeRequestId {
126        self.id
127    }
128
129    /// Non-blocking inspection for tests, embedders, and event-driven code.
130    pub fn try_result(&mut self) -> Option<PlatformOperationResult> {
131        self.receiver
132            .try_result(|| Err(PlatformOperationError::unavailable()))
133    }
134}
135
136impl Future for NativeOperationRequest {
137    type Output = PlatformOperationResult;
138    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
139        self.receiver
140            .poll_result(context, || Err(PlatformOperationError::unavailable()))
141    }
142}
143
144/// A thread-safe reference to a generational Incular window. Methods only
145/// enqueue data for the UI/event-loop turn; they never touch native state.
146#[derive(Clone)]
147pub struct WindowHandle {
148    pub(crate) id: WindowId,
149    pub(crate) bridge: Arc<WindowCommandBridge>,
150    pub(crate) file_dialogs: FileDialogService,
151    pub(crate) capabilities: Arc<RwLock<PlatformCapabilities>>,
152    pub(crate) observed_state: Arc<RwLock<WindowObservedState>>,
153    pub(crate) displays: Arc<RwLock<DisplayCatalog>>,
154    pub(crate) transient_presentations: Arc<RwLock<Vec<TransientPresentationResolution>>>,
155}
156
157impl std::fmt::Debug for WindowHandle {
158    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        formatter
160            .debug_tuple("WindowHandle")
161            .field(&self.id)
162            .finish()
163    }
164}
165
166impl WindowHandle {
167    #[must_use]
168    pub const fn id(&self) -> WindowId {
169        self.id
170    }
171
172    #[must_use]
173    pub fn capabilities(&self) -> PlatformCapabilities {
174        *self
175            .capabilities
176            .read()
177            .expect("window capability snapshot lock")
178    }
179
180    /// Native file/folder dialog service associated with this window. The
181    /// service retains only the Incular window identity; native parent handles
182    /// remain inside the desktop adapter.
183    #[must_use]
184    pub fn file_dialogs(&self) -> FileDialogService {
185        self.file_dialogs.clone()
186    }
187
188    #[must_use]
189    pub fn observed_state(&self) -> WindowObservedState {
190        *self
191            .observed_state
192            .read()
193            .expect("window observed-state snapshot lock")
194    }
195
196    /// Resolved native-vs-overlay presentation for visible retained transients
197    /// in this window. The native adapter updates this after host negotiation;
198    /// an empty list means no visible semantic transient is currently present.
199    #[must_use]
200    pub fn transient_presentations(&self) -> Vec<TransientPresentationResolution> {
201        self.transient_presentations
202            .read()
203            .expect("transient presentation snapshot lock")
204            .clone()
205    }
206
207    /// Immutable snapshots for displays currently published by the native
208    /// backend. Removed displays disappear immediately; a later display using
209    /// the same registry slot carries a different generation.
210    #[must_use]
211    pub fn displays(&self) -> Vec<DisplaySnapshot> {
212        self.displays.read().expect("display catalog lock").all()
213    }
214
215    #[must_use]
216    pub fn display(&self, id: incular_platform::DisplayId) -> Option<DisplaySnapshot> {
217        self.displays.read().expect("display catalog lock").get(id)
218    }
219
220    #[must_use]
221    pub fn primary_display(&self) -> Option<DisplaySnapshot> {
222        self.displays
223            .read()
224            .expect("display catalog lock")
225            .primary()
226    }
227
228    #[must_use]
229    pub fn current_display(&self) -> Option<DisplaySnapshot> {
230        let id = self.observed_state().current_display?;
231        self.display(id)
232    }
233
234    pub fn set_title(&self, title: impl Into<String>) -> Result<(), WindowCommandEnqueueError> {
235        self.send(WindowOperation::SetTitle(title.into()))
236    }
237
238    pub fn set_visible(&self, visible: bool) -> Result<(), WindowCommandEnqueueError> {
239        self.send(WindowOperation::SetVisible(visible))
240    }
241
242    pub fn request_logical_size(
243        &self,
244        size: incular_core::Size,
245    ) -> Result<(), WindowCommandEnqueueError> {
246        self.send(WindowOperation::SetLogicalSize(size))
247    }
248
249    pub fn set_outer_position(
250        &self,
251        position: PhysicalScreenPosition,
252    ) -> Result<(), WindowCommandEnqueueError> {
253        self.send(WindowOperation::SetOuterPosition(position))
254    }
255
256    /// Centers this window using already-observed native outer size and one
257    /// immutable display snapshot. Work-area centering never falls back to full
258    /// monitor bounds: callers must choose that policy explicitly.
259    pub fn center_on_display(
260        &self,
261        display: &DisplaySnapshot,
262        area: DisplayPlacementArea,
263    ) -> Result<PhysicalScreenPosition, WindowPlacementError> {
264        if self.capabilities().display.set_window_position == CapabilitySupport::Unsupported {
265            return Err(WindowPlacementError::Unsupported);
266        }
267        let outer_size = self
268            .observed_state()
269            .outer_size
270            .ok_or(WindowPlacementError::OuterSizeUnavailable)?;
271        let bounds = display
272            .placement_rect(area)
273            .ok_or(WindowPlacementError::DisplayGeometryUnavailable(area))?;
274        let target = bounds.centered_position(outer_size);
275        self.set_outer_position(target)?;
276        Ok(target)
277    }
278
279    /// Moves the window to a physical point relative to one known display.
280    /// This is intentionally unavailable when the display snapshot has no
281    /// desktop-global origin (for example a Wayland top-level display).
282    pub fn move_relative_to_display(
283        &self,
284        display: &DisplaySnapshot,
285        position: incular_platform::PhysicalDisplayPosition,
286    ) -> Result<PhysicalScreenPosition, WindowPlacementError> {
287        if self.capabilities().display.set_window_position == CapabilitySupport::Unsupported {
288            return Err(WindowPlacementError::Unsupported);
289        }
290        let target = display.screen_position(position).ok_or(
291            WindowPlacementError::DisplayGeometryUnavailable(DisplayPlacementArea::FullBounds),
292        )?;
293        self.set_outer_position(target)?;
294        Ok(target)
295    }
296
297    /// Begins a compositor/window-manager-owned interactive move. The native
298    /// backend may require this call to follow a real primary-button press.
299    pub fn begin_move_drag(&self) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
300        self.bridge.request(self.id, WindowOperation::BeginMoveDrag)
301    }
302
303    /// Begins a compositor/window-manager-owned interactive resize from one
304    /// edge/corner. Incular never emulates this by changing global coordinates.
305    pub fn begin_resize_drag(
306        &self,
307        direction: incular_core::WindowResizeDirection,
308    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
309        self.bridge
310            .request(self.id, WindowOperation::BeginResizeDrag(direction))
311    }
312
313    pub fn set_minimized(&self, minimized: bool) -> Result<(), WindowCommandEnqueueError> {
314        self.send(WindowOperation::SetMinimized(minimized))
315    }
316
317    pub fn set_maximized(&self, maximized: bool) -> Result<(), WindowCommandEnqueueError> {
318        self.send(WindowOperation::SetMaximized(maximized))
319    }
320
321    pub fn set_fullscreen(
322        &self,
323        fullscreen: Option<Fullscreen>,
324    ) -> Result<(), WindowCommandEnqueueError> {
325        self.send(WindowOperation::SetFullscreen(fullscreen))
326    }
327
328    pub fn set_resizable(&self, resizable: bool) -> Result<(), WindowCommandEnqueueError> {
329        self.send(WindowOperation::SetResizable(resizable))
330    }
331
332    pub fn set_decorations(&self, decorations: bool) -> Result<(), WindowCommandEnqueueError> {
333        self.send(WindowOperation::SetDecorations(decorations))
334    }
335
336    pub fn set_logical_size_limits(
337        &self,
338        limits: LogicalSizeLimits,
339    ) -> Result<(), WindowCommandEnqueueError> {
340        self.send(WindowOperation::SetLogicalSizeLimits(limits))
341    }
342
343    pub fn set_window_level(&self, level: WindowLevel) -> Result<(), WindowCommandEnqueueError> {
344        self.send(WindowOperation::SetWindowLevel(level))
345    }
346
347    pub fn set_window_icon(
348        &self,
349        icon: Option<WindowIcon>,
350    ) -> Result<(), WindowCommandEnqueueError> {
351        self.send(WindowOperation::SetWindowIcon(icon))
352    }
353
354    pub fn request_user_attention(
355        &self,
356        attention: UserAttentionType,
357    ) -> Result<(), WindowCommandEnqueueError> {
358        self.send(WindowOperation::RequestUserAttention(Some(attention)))
359    }
360
361    pub fn cancel_user_attention(&self) -> Result<(), WindowCommandEnqueueError> {
362        self.send(WindowOperation::RequestUserAttention(None))
363    }
364
365    /// Requests native cursor confinement/locking. The returned request
366    /// completes with a typed platform result; the runtime does not claim a
367    /// grab succeeded merely because the command was enqueued.
368    pub fn set_cursor_grab(
369        &self,
370        mode: CursorGrabMode,
371    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
372        self.bridge
373            .request(self.id, WindowOperation::SetCursorGrab(mode))
374    }
375
376    /// Shows or hides the native cursor for this window.
377    pub fn set_cursor_visible(
378        &self,
379        visible: bool,
380    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
381        self.bridge
382            .request(self.id, WindowOperation::SetCursorVisible(visible))
383    }
384
385    /// Warps the native cursor to a validated logical client-area position.
386    pub fn set_cursor_position(
387        &self,
388        position: LogicalWindowPosition,
389    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
390        self.bridge
391            .request(self.id, WindowOperation::SetCursorPosition(position))
392    }
393
394    /// Requests the window's normalized content-capture policy. The command
395    /// is applied by the native adapter on its event-loop thread. Unsupported
396    /// adapters resolve the returned request with a typed platform error.
397    pub fn set_content_sensitivity(
398        &self,
399        sensitivity: incular_config::ContentSensitivity,
400    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
401        self.bridge
402            .request(self.id, WindowOperation::SetContentSensitivity(sensitivity))
403    }
404
405    pub fn request_focus(&self) -> Result<(), WindowCommandEnqueueError> {
406        self.send(WindowOperation::RequestFocus)
407    }
408
409    pub fn request_redraw(&self) -> Result<(), WindowCommandEnqueueError> {
410        self.send(WindowOperation::RequestRedraw)
411    }
412
413    pub fn close(&self) -> Result<(), WindowCommandEnqueueError> {
414        self.send(WindowOperation::Close)
415    }
416
417    fn send(&self, operation: WindowOperation) -> Result<(), WindowCommandEnqueueError> {
418        self.bridge.send(WindowCommand::new(self.id, operation))
419    }
420}
421
422/// Cloneable UI-side capability for opening retained windows after startup.
423/// It contains no native handle and can be retained by button callbacks; native
424/// creation is still queued for the active desktop event-loop callback.
425#[derive(Clone)]
426pub struct WindowOpener {
427    pub(crate) manager: WindowManager,
428}
429
430impl WindowOpener {
431    pub fn open_window(
432        &self,
433        options: WindowOptions,
434        root: Widget,
435    ) -> Result<WindowHandle, WindowError> {
436        self.manager.open_window(options, root)
437    }
438
439    pub fn open_window_with(
440        &self,
441        options: WindowOptions,
442        build: impl FnMut(&mut BuildContext) -> Widget + 'static,
443    ) -> Result<WindowHandle, WindowError> {
444        self.manager.open_window_with(options, build)
445    }
446
447    /// Opens a window whose descriptor is persisted under a stable
448    /// application-provided ID. Its factory still lives in application code;
449    /// no widget or native handle is serialized.
450    pub fn open_restorable_window_with(
451        &self,
452        restoration_id: WindowRestorationId,
453        kind: impl Into<String>,
454        options: WindowOptions,
455        build: impl FnMut(&mut BuildContext) -> Widget + 'static,
456    ) -> Result<WindowHandle, WindowError> {
457        self.manager
458            .open_restorable_window_with(restoration_id, kind, options, build)
459    }
460}
461
462pub(crate) struct QueuedNativeRequest {
463    pub(crate) sender: oneshot::Sender<PlatformOperationResult>,
464    pub(crate) cancelled: Arc<AtomicBool>,
465}
466
467pub(crate) struct QueuedWindowCommand {
468    pub(crate) command: WindowCommand,
469    pub(crate) request: Option<QueuedNativeRequest>,
470}
471
472pub(crate) struct WindowCommandBridge {
473    channel: RequestChannel<QueuedWindowCommand>,
474    cancellation_sender: mpsc::Sender<NativeRequestId>,
475}
476
477impl WindowCommandBridge {
478    pub(crate) fn new(
479        sender: mpsc::Sender<QueuedWindowCommand>,
480        cancellation_sender: mpsc::Sender<NativeRequestId>,
481    ) -> Self {
482        Self {
483            channel: RequestChannel::new(sender),
484            cancellation_sender,
485        }
486    }
487    pub(crate) fn send(&self, command: WindowCommand) -> Result<(), WindowCommandEnqueueError> {
488        self.channel
489            .send(QueuedWindowCommand {
490                command,
491                request: None,
492            })
493            .map_err(channel_error)
494    }
495    pub(crate) fn request(
496        self: &Arc<Self>,
497        window_id: WindowId,
498        operation: WindowOperation,
499    ) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
500        self.channel
501            .request(|id| {
502                let request_id = NativeRequestId::new(id);
503                let (sender, receiver) = oneshot::channel();
504                let cancelled = Arc::new(AtomicBool::new(false));
505                let queued = QueuedWindowCommand {
506                    command: WindowCommand::with_request(window_id, request_id, operation),
507                    request: Some(QueuedNativeRequest {
508                        sender,
509                        cancelled: cancelled.clone(),
510                    }),
511                };
512                let bridge = self.clone();
513                let receiver = RequestReceiver::new(receiver, move || {
514                    cancelled.store(true, Ordering::Release);
515                    bridge.cancel_request(request_id);
516                });
517                (
518                    queued,
519                    NativeOperationRequest {
520                        id: request_id,
521                        receiver,
522                    },
523                )
524            })
525            .map_err(channel_error)
526    }
527    pub(crate) fn cancel_request(&self, request_id: NativeRequestId) {
528        self.channel.cancel(&self.cancellation_sender, request_id);
529    }
530    pub(crate) fn stop(&self) {
531        self.channel.stop();
532    }
533    pub(crate) fn set_wake(&self, wake: Arc<dyn RuntimeWake>) {
534        self.channel.set_wake(wake);
535    }
536}
537fn channel_error(error: ChannelError) -> WindowCommandEnqueueError {
538    match error {
539        ChannelError::Stopped => WindowCommandEnqueueError::RuntimeStopped,
540        ChannelError::IdExhausted => WindowCommandEnqueueError::RequestIdExhausted,
541    }
542}