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#[derive(Clone, Debug, PartialEq)]
33pub enum NativeWindowCommand {
34 Create {
35 window_id: WindowId,
36 options: WindowOptions,
37 },
38 Operate(WindowCommand),
39}
40
41#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum NativeOperationCompletionStatus {
106 Completed,
107 UnknownRequest,
108 TargetMismatch,
109 StaleTarget,
110}
111
112#[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 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#[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 #[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 #[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 #[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 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 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 pub fn begin_move_drag(&self) -> Result<NativeOperationRequest, WindowCommandEnqueueError> {
300 self.bridge.request(self.id, WindowOperation::BeginMoveDrag)
301 }
302
303 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 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 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 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 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#[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 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}