1use 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#[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#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
100pub struct WindowId {
101 index: u32,
102 generation: u32,
103}
104
105impl WindowId {
106 #[must_use]
111 pub const fn from_parts(index: u32, generation: u32) -> Self {
112 Self { index, generation }
113 }
114
115 #[must_use]
117 pub const fn index(self) -> u32 {
118 self.index
119 }
120
121 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum Fullscreen {
146 Borderless,
147}
148
149#[derive(Clone, Debug, PartialEq)]
156pub struct WindowOptions {
157 pub title: String,
158 pub initial_logical_size: Size,
159 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 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 #[must_use]
207 pub fn new(title: impl Into<String>) -> Self {
208 Self {
209 title: title.into(),
210 ..Self::default()
211 }
212 }
213
214 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#[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#[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 #[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#[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 SetContentSensitivity(incular_config::ContentSensitivity),
372 RequestFocus,
373 RequestRedraw,
374 Close,
375}
376
377#[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#[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 #[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#[derive(Clone, Debug, PartialEq)]
441pub enum WindowEventKind {
442 Platform(PlatformEvent),
444 Lifecycle(WindowLifecycle),
446 RedrawRequested,
448 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#[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#[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#[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
596pub trait TextInputAdapter {
598 fn apply(&mut self, command: &TextInputCommand);
599}
600
601#[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 TextInputAction(TextInputAction),
632 Metrics(WindowMetrics),
633 Environment(incular_config::RuntimeEnvironment),
636 Lifecycle(PlatformLifecycle),
637 ExternalDrag(ExternalDragEvent),
638 CloseRequested,
639}
640
641#[derive(Clone, Copy, Debug, PartialEq, Eq)]
644pub enum PlatformLifecycle {
645 Active,
646 Inactive,
647 Suspended,
648 Resumed,
652 Stopping,
653}
654#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
658pub enum NativeWindowSystem {
659 Win32,
660 AppKit,
661 X11,
662 Wayland,
663 Other,
664}