Skip to main content

incular_platform/
lib.rs

1//! Platform-owned window data and normalized native events.
2//!
3//! Layout stays in logical pixels. This crate is the single conversion boundary
4//! between those coordinates and physical surface/window coordinates.
5
6use directories::ProjectDirs;
7use incular_config::ApplicationDefaults;
8use incular_core::{Color, InputEvent, Offset, Rect, Size};
9use std::{fmt, path::PathBuf};
10
11mod application_activation;
12mod application_shell;
13mod capabilities;
14mod content_sensitivity;
15mod data_transfer;
16mod display;
17mod file_dialog;
18mod operation;
19mod pointer;
20mod system_environment;
21mod window_control;
22
23pub use application_activation::{
24    ApplicationActivation, GlobalShortcutChord, GlobalShortcutChordError, GlobalShortcutError,
25    GlobalShortcutId, LaunchActivation, SingleInstancePolicy, SingleInstancePolicyError,
26    UrlActivation,
27};
28pub use application_shell::{
29    ApplicationBadge, ApplicationShellError, ApplicationShellFeature, NotificationAction,
30    NotificationActionId, NotificationId, NotificationPresentation, TaskbarDockState,
31    TaskbarProgress, TaskbarProgressError, TrayItemId, TrayItemPresentation,
32};
33pub use capabilities::{
34    AdvancedInputCapabilities, ApplicationServiceCapabilities, CapabilitySupport,
35    DataTransferCapabilities, DisplayPlacementCapabilities, NativeMenuCapabilities,
36    PlatformCapabilities, TransientSurfaceCapabilities, WindowControlCapabilities,
37};
38pub use content_sensitivity::{
39    ContentSensitivityBackend, ContentSensitivityCapability, ContentSensitivityNoOpReason,
40    ContentSensitivityOutcome, MemoryContentSensitivityBackend, NoopContentSensitivityBackend,
41};
42pub use data_transfer::{
43    Clipboard, ClipboardCapabilities, ClipboardError, ClipboardWriteReport, DataTransfer,
44    ExternalDragEvent, ExternalDragPhase, ExternalDragResponse, MediaType, MemoryClipboard,
45    TransferData, TransferDataError, TransferFormat, TransferFormatCapabilities, TransferImage,
46    TransferItem, TransferOperation, TransferOperations, TransferReadRequest,
47    TransferRepresentation,
48};
49pub use display::{
50    DisplayId, DisplayPlacementArea, DisplaySnapshot, LogicalDisplayPosition,
51    LogicalScreenPosition, LogicalScreenRect, PhysicalDisplayPosition, PhysicalScreenPosition,
52    PhysicalScreenRect,
53};
54pub use file_dialog::{
55    DocumentActivation, DocumentDescriptor, FileContentType, FileContentTypeKind,
56    FileDialogCapabilities, FileDialogError, FileDialogFilter, FileDialogFilterId, FileDialogKind,
57    FileDialogOption, FileDialogOptions, FileDialogOutcome, FileDialogRequest, FileDialogRequestId,
58    FileDialogSelection, FileDialogValidationError, FileExtension,
59};
60pub use incular_config::{TransparencyMode, WindowSizePolicy};
61pub use operation::{
62    NativeOperationCompletion, NativeRequestId, PlatformOperationError, PlatformOperationErrorKind,
63    PlatformOperationResult,
64};
65pub use pointer::{
66    CursorGrabMode, LogicalWindowPosition, LogicalWindowPositionError, NativePointerSample,
67    PointerMetadata,
68};
69pub use system_environment::{
70    MemorySystemEnvironmentProvider, SystemEnvironmentPreferences, SystemEnvironmentProvider,
71    canonicalize_system_locales,
72};
73pub use window_control::{
74    LogicalSizeLimits, LogicalSizeLimitsError, UserAttentionType, WindowIcon, WindowIconError,
75    WindowLevel, WindowObservedState, WindowRequestedState,
76};
77
78/// Resolves the persistent, local application-data directory through the
79/// operating system's standard project-directory conventions.
80///
81/// The caller supplies a stable application identifier (normally a reverse
82/// domain ID).  Incular intentionally never derives it from a window title,
83/// executable path, current directory, or transient window ID.  Restoration
84/// snapshots belong in local data rather than the regenerable cache directory.
85#[must_use]
86pub fn application_data_local_directory(application_id: &str) -> Option<PathBuf> {
87    (!application_id.trim().is_empty())
88        .then(|| ProjectDirs::from("org", "Incular", application_id))
89        .flatten()
90        .map(|directories| directories.data_local_dir().to_path_buf())
91}
92
93/// Stable identity for an Incular window.
94///
95/// A [`WindowId`] is deliberately unrelated to a native window handle. Window
96/// registries allocate an index and advance its generation whenever that slot
97/// is reused, so a command retained for a closed window cannot affect the next
98/// window occupying the same slot.
99#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
100pub struct WindowId {
101    index: u32,
102    generation: u32,
103}
104
105impl WindowId {
106    /// Creates an ID from a registry slot and its current generation.
107    ///
108    /// This is public for platform/runtime registries, not for representing a
109    /// native ID. Application code normally obtains IDs from `WindowHandle`.
110    #[must_use]
111    pub const fn from_parts(index: u32, generation: u32) -> Self {
112        Self { index, generation }
113    }
114
115    /// Returns the stable slot index selected by the owning window registry.
116    #[must_use]
117    pub const fn index(self) -> u32 {
118        self.index
119    }
120
121    /// Returns the slot generation used to reject stale handles and commands.
122    #[must_use]
123    pub const fn generation(self) -> u32 {
124        self.generation
125    }
126}
127
128impl fmt::Debug for WindowId {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(formatter, "WindowId({}, {})", self.index, self.generation)
131    }
132}
133
134impl fmt::Display for WindowId {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(formatter, "Window#{}@{}", self.index + 1, self.generation)
137    }
138}
139
140/// The portable subset of fullscreen behaviour supported by Incular.
141///
142/// Borderless fullscreen intentionally avoids exposing platform monitor/video
143/// mode handles. Native adapters may use the monitor containing the window.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum Fullscreen {
146    Borderless,
147}
148
149/// Configuration used when creating an Incular desktop window.
150///
151/// All sizes are logical pixels. A transparent window is a strict presentation
152/// request: a backend must either configure compatible compositor alpha or
153/// report that the request is unsupported rather than silently presenting an
154/// opaque/black surface.
155#[derive(Clone, Debug, PartialEq)]
156pub struct WindowOptions {
157    pub title: String,
158    pub initial_logical_size: Size,
159    /// Selects whether native viewport metrics or retained content own size.
160    ///
161    /// In [`WindowSizePolicy::Content`] mode `initial_logical_size` is also the
162    /// minimum content viewport. The framework follows the retained root's
163    /// layout size and can shrink back when primary content becomes smaller.
164    /// Paint-only overflow such as shadows and transient overlays does not
165    /// resize the native host. This is deliberately independent of window
166    /// decorations.
167    pub size_policy: WindowSizePolicy,
168    pub minimum_logical_size: Option<Size>,
169    pub maximum_logical_size: Option<Size>,
170    pub resizable: bool,
171    pub visible: bool,
172    pub decorations: bool,
173    pub transparency_mode: TransparencyMode,
174    /// Base scene color. This does not change native compositor transparency.
175    pub background_color: Color,
176    pub maximized: bool,
177    pub fullscreen: Option<Fullscreen>,
178    pub window_level: WindowLevel,
179    pub window_icon: Option<WindowIcon>,
180}
181
182impl Default for WindowOptions {
183    fn default() -> Self {
184        let defaults = ApplicationDefaults::DEFAULT;
185        Self {
186            title: defaults.window_title.to_owned(),
187            initial_logical_size: defaults.initial_window_size,
188            size_policy: defaults.window_size_policy,
189            minimum_logical_size: None,
190            maximum_logical_size: None,
191            resizable: defaults.resizable,
192            visible: defaults.visible,
193            decorations: defaults.decorations,
194            transparency_mode: defaults.transparency_mode,
195            background_color: defaults.background_color,
196            maximized: defaults.maximized,
197            fullscreen: None,
198            window_level: WindowLevel::Normal,
199            window_icon: None,
200        }
201    }
202}
203
204impl WindowOptions {
205    /// Creates options with the documented desktop defaults and a chosen title.
206    #[must_use]
207    pub fn new(title: impl Into<String>) -> Self {
208        Self {
209            title: title.into(),
210            ..Self::default()
211        }
212    }
213
214    /// Checks size constraints before a native adapter attempts window creation.
215    pub fn validate(&self) -> Result<(), WindowOptionsError> {
216        if !is_positive_size(self.initial_logical_size) {
217            return Err(WindowOptionsError::InvalidInitialSize);
218        }
219        if self
220            .minimum_logical_size
221            .is_some_and(|size| !is_positive_size(size))
222        {
223            return Err(WindowOptionsError::InvalidMinimumSize);
224        }
225        if self
226            .maximum_logical_size
227            .is_some_and(|size| !is_positive_size(size))
228        {
229            return Err(WindowOptionsError::InvalidMaximumSize);
230        }
231        if let (Some(minimum), Some(maximum)) =
232            (self.minimum_logical_size, self.maximum_logical_size)
233            && (minimum.width > maximum.width || minimum.height > maximum.height)
234        {
235            return Err(WindowOptionsError::MinimumExceedsMaximum);
236        }
237        if self
238            .minimum_logical_size
239            .is_some_and(|minimum| !size_is_at_least(self.initial_logical_size, minimum))
240        {
241            return Err(WindowOptionsError::InitialSizeBelowMinimum);
242        }
243        if self
244            .maximum_logical_size
245            .is_some_and(|maximum| !size_is_at_least(maximum, self.initial_logical_size))
246        {
247            return Err(WindowOptionsError::InitialSizeAboveMaximum);
248        }
249        Ok(())
250    }
251
252    pub fn requested_state(&self) -> Result<WindowRequestedState, LogicalSizeLimitsError> {
253        let size_limits =
254            LogicalSizeLimits::new(self.minimum_logical_size, self.maximum_logical_size)?;
255        Ok(WindowRequestedState {
256            visible: self.visible,
257            minimized: false,
258            maximized: self.maximized,
259            fullscreen: self.fullscreen,
260            resizable: self.resizable,
261            decorations: self.decorations,
262            size_limits,
263            level: self.window_level,
264        })
265    }
266}
267
268fn is_positive_size(size: Size) -> bool {
269    size.width.is_finite() && size.height.is_finite() && size.width > 0.0 && size.height > 0.0
270}
271
272fn size_is_at_least(left: Size, right: Size) -> bool {
273    left.width >= right.width && left.height >= right.height
274}
275
276/// A contradiction in portable window-creation constraints.
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum WindowOptionsError {
279    InvalidInitialSize,
280    InvalidMinimumSize,
281    InvalidMaximumSize,
282    MinimumExceedsMaximum,
283    InitialSizeBelowMinimum,
284    InitialSizeAboveMaximum,
285}
286
287impl fmt::Display for WindowOptionsError {
288    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
289        let description = match self {
290            Self::InvalidInitialSize => "initial logical size must be finite and positive",
291            Self::InvalidMinimumSize => "minimum logical size must be finite and positive",
292            Self::InvalidMaximumSize => "maximum logical size must be finite and positive",
293            Self::MinimumExceedsMaximum => {
294                "minimum logical size cannot exceed maximum logical size"
295            }
296            Self::InitialSizeBelowMinimum => {
297                "initial logical size cannot be smaller than the minimum logical size"
298            }
299            Self::InitialSizeAboveMaximum => {
300                "initial logical size cannot be larger than the maximum logical size"
301            }
302        };
303        formatter.write_str(description)
304    }
305}
306
307impl std::error::Error for WindowOptionsError {}
308
309/// A UI-thread operation requested for one Incular window.
310///
311/// Commands are data only: Tokio workers can enqueue them through the runtime
312/// UI dispatcher, while native adapters apply them from their event-loop turn.
313#[derive(Clone, Debug, PartialEq)]
314pub struct WindowCommand {
315    pub window_id: WindowId,
316    pub operation: WindowOperation,
317    pub request_id: Option<NativeRequestId>,
318}
319
320impl WindowCommand {
321    #[must_use]
322    pub const fn new(window_id: WindowId, operation: WindowOperation) -> Self {
323        Self {
324            window_id,
325            operation,
326            request_id: None,
327        }
328    }
329
330    /// Associates this command with one result-bearing runtime request.
331    #[must_use]
332    pub const fn with_request(
333        window_id: WindowId,
334        request_id: NativeRequestId,
335        operation: WindowOperation,
336    ) -> Self {
337        Self {
338            window_id,
339            operation,
340            request_id: Some(request_id),
341        }
342    }
343}
344
345/// Operations available through an Incular `WindowHandle`.
346///
347/// `Close` is an accepted application close operation. A native close gesture
348/// is reported first as [`PlatformEvent::CloseRequested`] in a [`WindowEvent`]
349/// so applications can accept or cancel it.
350#[derive(Clone, Debug, PartialEq)]
351pub enum WindowOperation {
352    SetTitle(String),
353    SetVisible(bool),
354    SetLogicalSize(Size),
355    SetOuterPosition(PhysicalScreenPosition),
356    BeginMoveDrag,
357    BeginResizeDrag(incular_core::WindowResizeDirection),
358    SetMinimized(bool),
359    SetMaximized(bool),
360    SetFullscreen(Option<Fullscreen>),
361    SetResizable(bool),
362    SetDecorations(bool),
363    SetLogicalSizeLimits(LogicalSizeLimits),
364    SetWindowLevel(WindowLevel),
365    SetWindowIcon(Option<WindowIcon>),
366    RequestUserAttention(Option<UserAttentionType>),
367    SetCursorGrab(CursorGrabMode),
368    SetCursorVisible(bool),
369    SetCursorPosition(LogicalWindowPosition),
370    /// Applies the retained tree's effective capture-protection policy.
371    SetContentSensitivity(incular_config::ContentSensitivity),
372    RequestFocus,
373    RequestRedraw,
374    Close,
375}
376
377/// Native lifecycle state for one window, independent of application lifecycle.
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
379pub enum WindowLifecycle {
380    Creating,
381    Visible,
382    Hidden,
383    Focused,
384    Unfocused,
385    Closing,
386    Closed,
387}
388
389/// A native event routed to one normalized Incular window.
390///
391/// Existing [`PlatformEvent`] values remain unchanged and are carried by the
392/// `Platform` variant. This lets a single-window runtime keep using
393/// `PlatformEvent` while a multi-window runtime associates every input,
394/// metrics update, and close request with a [`WindowId`].
395#[derive(Clone, Debug, PartialEq)]
396pub struct WindowEvent {
397    pub window_id: WindowId,
398    pub kind: WindowEventKind,
399}
400
401impl WindowEvent {
402    #[must_use]
403    pub const fn new(window_id: WindowId, kind: WindowEventKind) -> Self {
404        Self { window_id, kind }
405    }
406
407    #[must_use]
408    pub fn platform(window_id: WindowId, event: PlatformEvent) -> Self {
409        Self::new(window_id, WindowEventKind::Platform(event))
410    }
411
412    #[must_use]
413    pub const fn lifecycle(window_id: WindowId, lifecycle: WindowLifecycle) -> Self {
414        Self::new(window_id, WindowEventKind::Lifecycle(lifecycle))
415    }
416
417    #[must_use]
418    pub const fn redraw_requested(window_id: WindowId) -> Self {
419        Self::new(window_id, WindowEventKind::RedrawRequested)
420    }
421
422    #[must_use]
423    pub const fn state_changed(window_id: WindowId, state: WindowObservedState) -> Self {
424        Self::new(window_id, WindowEventKind::StateChanged(state))
425    }
426
427    /// Returns the wrapped legacy event when this is a platform event.
428    #[must_use]
429    pub fn platform_event(&self) -> Option<&PlatformEvent> {
430        match &self.kind {
431            WindowEventKind::Platform(event) => Some(event),
432            WindowEventKind::Lifecycle(_)
433            | WindowEventKind::RedrawRequested
434            | WindowEventKind::StateChanged(_) => None,
435        }
436    }
437}
438
439/// Window event data that is not part of the legacy [`PlatformEvent`] stream.
440#[derive(Clone, Debug, PartialEq)]
441pub enum WindowEventKind {
442    /// Existing input, metrics, application lifecycle, or close-request data.
443    Platform(PlatformEvent),
444    /// A state transition of the native window itself.
445    Lifecycle(WindowLifecycle),
446    /// The native surface is ready for the target window to present a frame.
447    RedrawRequested,
448    /// Snapshot of native state that the active backend can actually observe.
449    StateChanged(WindowObservedState),
450}
451
452#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
453pub struct PhysicalSize {
454    pub width: u32,
455    pub height: u32,
456}
457impl PhysicalSize {
458    #[must_use]
459    pub const fn new(width: u32, height: u32) -> Self {
460        Self { width, height }
461    }
462    #[must_use]
463    pub const fn is_zero(self) -> bool {
464        self.width == 0 || self.height == 0
465    }
466}
467#[derive(Clone, Copy, Debug, PartialEq)]
468pub struct WindowMetrics {
469    pub physical_size: PhysicalSize,
470    pub scale_factor: f64,
471}
472impl WindowMetrics {
473    #[must_use]
474    pub fn new(physical_size: PhysicalSize, scale_factor: f64) -> Self {
475        assert!(
476            scale_factor.is_finite() && scale_factor > 0.0,
477            "scale factor must be positive"
478        );
479        Self {
480            physical_size,
481            scale_factor,
482        }
483    }
484    #[must_use]
485    pub fn logical_size(self) -> Size {
486        Size::new(
487            self.physical_size.width as f32 / self.scale_factor as f32,
488            self.physical_size.height as f32 / self.scale_factor as f32,
489        )
490    }
491    #[must_use]
492    pub fn logical_to_physical(self, offset: Offset) -> Offset {
493        Offset::new(
494            offset.x * self.scale_factor as f32,
495            offset.y * self.scale_factor as f32,
496        )
497    }
498    #[must_use]
499    pub fn physical_to_logical(self, offset: Offset) -> Offset {
500        Offset::new(
501            offset.x / self.scale_factor as f32,
502            offset.y / self.scale_factor as f32,
503        )
504    }
505}
506
507/// Stable identity for the text-input client currently attached to a window.
508/// It is generated by the retained tree and is independent of native handles.
509#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
510pub struct TextInputClientId(pub u64);
511
512impl TextInputClientId {
513    #[must_use]
514    pub const fn new(value: u64) -> Self {
515        Self(value)
516    }
517
518    #[must_use]
519    pub const fn get(self) -> u64 {
520        self.0
521    }
522}
523
524/// Hints supplied to a native IME or soft-keyboard adapter.
525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
526pub enum TextInputType {
527    #[default]
528    Text,
529    Multiline,
530    Number,
531    Phone,
532    Email,
533    Url,
534    Password,
535}
536
537#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
538pub enum TextInputAction {
539    #[default]
540    Unspecified,
541    None,
542    Done,
543    Go,
544    Search,
545    Send,
546    Next,
547    Previous,
548    Newline,
549}
550
551#[derive(Clone, Debug, PartialEq, Eq)]
552pub struct TextInputConfiguration {
553    pub client: TextInputClientId,
554    pub input_type: TextInputType,
555    pub action: TextInputAction,
556    pub multiline: bool,
557    pub enabled: bool,
558    pub read_only: bool,
559    pub obscure_text: bool,
560}
561
562#[derive(Clone, Debug, Default, PartialEq, Eq)]
563pub struct TextInputState {
564    pub text: String,
565    pub selection_start: usize,
566    pub selection_end: usize,
567    pub composing: Option<(usize, usize)>,
568}
569
570/// Commands emitted by the runtime and consumed by a native text-input
571/// adapter. They contain no window handles and can be queued safely at the
572/// runtime boundary.
573#[derive(Clone, Debug, PartialEq)]
574pub enum TextInputCommand {
575    SetClient {
576        configuration: TextInputConfiguration,
577        state: TextInputState,
578        caret_rect: Rect,
579    },
580    Update {
581        client: TextInputClientId,
582        state: TextInputState,
583    },
584    SetCaretRect {
585        client: TextInputClientId,
586        rect: Rect,
587    },
588    Hide {
589        client: TextInputClientId,
590    },
591    Clear {
592        client: TextInputClientId,
593    },
594}
595
596/// Minimal adapter seam for embedders that own their own text-input bridge.
597pub trait TextInputAdapter {
598    fn apply(&mut self, command: &TextInputCommand);
599}
600
601/// In-memory adapter useful for tests and headless hosts.
602#[derive(Clone, Debug, Default, PartialEq)]
603pub struct MemoryTextInputAdapter {
604    commands: Vec<TextInputCommand>,
605}
606
607impl MemoryTextInputAdapter {
608    #[must_use]
609    pub fn commands(&self) -> &[TextInputCommand] {
610        &self.commands
611    }
612
613    pub fn clear(&mut self) {
614        self.commands.clear();
615    }
616}
617
618impl TextInputAdapter for MemoryTextInputAdapter {
619    fn apply(&mut self, command: &TextInputCommand) {
620        self.commands.push(command.clone());
621    }
622}
623
624#[derive(Clone, Debug, PartialEq)]
625pub enum PlatformEvent {
626    Input(InputEvent),
627    /// Action chosen by a native software keyboard (for example Done, Next,
628    /// or Search). Desktop backends normally express the same behavior as a
629    /// physical Enter key, while mobile hosts can forward the IME action
630    /// without manufacturing a keyboard event.
631    TextInputAction(TextInputAction),
632    Metrics(WindowMetrics),
633    /// Complete normalized environment replacement produced by the native
634    /// shell. Runtime dependency tracking decides which consumers invalidate.
635    Environment(incular_config::RuntimeEnvironment),
636    Lifecycle(PlatformLifecycle),
637    ExternalDrag(ExternalDragEvent),
638    CloseRequested,
639}
640
641/// Lifecycle states native adapters can report without depending on the
642/// runtime crate. Adapters may emit only the states their OS exposes.
643#[derive(Clone, Copy, Debug, PartialEq, Eq)]
644pub enum PlatformLifecycle {
645    Active,
646    Inactive,
647    Suspended,
648    /// The OS reports that a previously suspended application is runnable
649    /// again. Runtime maps this to `ApplicationLifecycle::Active` while keeping
650    /// the native transition explicit at the platform boundary.
651    Resumed,
652    Stopping,
653}
654/// Native desktop window-system family for capability refinement.
655///
656/// This is a backend diagnostic/capability value, not a public native handle.
657#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
658pub enum NativeWindowSystem {
659    Win32,
660    AppKit,
661    X11,
662    Wayland,
663    Other,
664}