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