Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/core/mod.rs
1mod action;
2mod app;
3mod autotracking;
4mod entity;
5mod model;
6mod view;
7mod window;
8 
9pub use action::*;
10pub use app::*;
11pub use autotracking::Tracked;
12pub use entity::*;
13pub use model::*;
14pub use view::*;
15pub use window::*;
16 
17use crate::platform::{self, FullscreenState, WindowBounds, WindowStyle};
18use crate::{keymap, Element};
19use anyhow::Error;
20 
21use crate::rendering::OnGPUDeviceSelected;
22use derivative::Derivative;
23use futures_util::future::BoxFuture;
24use pathfinder_geometry::rect::RectF;
25use serde::{Deserialize, Serialize};
26use std::path::Path;
27use std::rc::Rc;
28use std::time::Duration;
29use std::{
30 any::{Any, TypeId},
31 collections::{HashMap, HashSet},
32 fmt::{self, Debug},
33 hash::Hash,
34 mem,
35 sync::{atomic::AtomicUsize, atomic::Ordering},
36};
37 
38/// A unique identifier for a display.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct DisplayId(usize);
41 
42impl From<usize> for DisplayId {
43 fn from(value: usize) -> Self {
44 DisplayId(value)
45 }
46}
47 
48/// Index of a valid display. Note that this only denotes the index of a display
49/// in the list of active displays and is not a unique identifier.
50#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "schema_gen", derive(schemars::JsonSchema))]
52#[cfg_attr(
53 feature = "schema_gen",
54 schemars(
55 description = "Which display to use when multiple monitors are connected.",
56 rename_all = "snake_case"
57 )
58)]
59pub enum DisplayIdx {
60 /// The primary display of the user.
61 #[cfg_attr(
62 feature = "schema_gen",
63 schemars(description = "The primary (main) display.")
64 )]
65 Primary,
66 /// An external display at a given index.
67 #[cfg_attr(
68 feature = "schema_gen",
69 schemars(description = "An external display, identified by index.")
70 )]
71 External(usize),
72}
73 
74impl DisplayIdx {
75 // If the current DisplayIdx is still valid given the number of displays user has.
76 pub fn is_valid_given_display_count(&self, display_count: usize) -> bool {
77 match self {
78 DisplayIdx::Primary => display_count >= 1,
79 // Assumption here is we will always have one primary display -- any
80 // external display count should be on top of it.
81 DisplayIdx::External(idx) => display_count > *idx + 1,
82 }
83 }
84}
85 
86impl fmt::Display for DisplayIdx {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 DisplayIdx::Primary => write!(f, "Main Screen"),
90 // The naming convention here is for the first external display External(0),
91 // we should name it "Screen 2" and incrementally for the following displays.
92 DisplayIdx::External(idx) => write!(f, "Screen {}", idx + 2),
93 }
94 }
95}
96 
97/// Information to display the IME editor near the active cursor.
98#[derive(Debug)]
99pub struct CursorInfo {
100 /// Position of the active cursor.
101 pub position: RectF,
102 /// The font size tells us how far below the active cursor position we place the IME.
103 pub font_size: f32,
104}
105 
106#[derive(Debug)]
107pub struct ApplicationBundleInfo<'a> {
108 pub path: &'a Path,
109 // Executable path could be None if the application does not have an executable.
110 pub executable: Option<&'a Path>,
111}
112 
113// An TimerId is a globally unique id for a timer associated with a callback
114#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
115pub struct TaskId(usize);
116 
117impl TaskId {
118 /// \return the next view ID. Note the first return is 0.
119 #[allow(clippy::new_without_default)]
120 pub fn new() -> TaskId {
121 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
122 let raw = NEXT_ID.fetch_add(1, Ordering::Relaxed);
123 TaskId(raw)
124 }
125}
126 
127impl fmt::Display for TaskId {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 std::fmt::Display::fmt(&self.0, f)
130 }
131}
132 
133pub type OptionalPlatformWindow = Option<Rc<dyn platform::Window>>;
134 
135type ActionCallback =
136 dyn FnMut(&mut dyn AnyView, &dyn Any, &mut AppContext, WindowId, EntityId) -> bool;
137 
138type TypedActionCallback =
139 dyn FnMut(&mut dyn AnyView, &dyn Any, &mut AppContext, WindowId, EntityId);
140 
141type GlobalActionCallback =
142 dyn FnMut(&dyn Any, &'static std::panic::Location<'static>, &mut AppContext);
143 
144type InvalidationCallback = dyn FnMut(WindowId, &mut AppContext);
145 
146#[derive(PartialEq, Eq, Hash, Debug)]
147struct ViewType(TypeId);
148 
149impl ViewType {
150 fn of<T: ?Sized + 'static>() -> Self {
151 ViewType(TypeId::of::<T>())
152 }
153}
154 
155// Helper struct for defining actions bound to a global shortcut/hotkey.
156struct GlobalShortcut {
157 action: &'static str,
158 args: Box<dyn Any>,
159}
160 
161#[derive(Default, Derivative)]
162#[derivative(Debug)]
163pub struct AddWindowOptions {
164 pub background_blur_radius_pixels: Option<u8>,
165 pub background_blur_texture: bool,
166 pub window_style: WindowStyle,
167 pub window_bounds: WindowBounds,
168 pub title: Option<String>,
169 pub fullscreen_state: FullscreenState,
170 
171 /// If true, new windows created immediately after this window is closed
172 /// will have the same position and size as this window.
173 pub anchor_new_windows_from_closed_position: NextNewWindowsHasThisWindowsBoundsUponClose,
174 /// The callback to be called when the GPU driver this window will render to is selected.
175 #[derivative(Debug = "ignore")]
176 pub on_gpu_driver_selected: Option<Box<OnGPUDeviceSelected>>,
177 /// This is a name to distinguish different windows among one application. It is a no-op on all
178 /// platforms except X11 Linux. See docs on the "WM_CLASS" property:
179 /// https://www.x.org/docs/ICCCM/icccm.pdf
180 pub window_instance: Option<String>,
181}
182 
183#[derive(Debug, Default)]
184pub enum NextNewWindowsHasThisWindowsBoundsUponClose {
185 /// Create the next new window with the position and size of this window if it's been closed.
186 #[default]
187 Yes,
188 
189 /// Ignore the bounds of this window when creating the next new one.
190 No,
191}
192 
193pub(crate) type SpawnedFuture = BoxFuture<'static, ()>;
194 
195#[derive(Debug, Default, Clone)]
196pub struct WindowInvalidation {
197 pub updated: HashSet<EntityId>,
198 pub removed: HashSet<EntityId>,
199 /// Stores whether an element in the window needs to be repainted. Currently an
200 /// invalidation will repaint the entire element tree for that window, so we
201 /// only store a boolean. In the future we can extend this to store entity ids
202 /// for specific views that need to be redrawn once we have that capability.
203 pub redraw_requested: bool,
204}
205 
206pub enum Effect {
207 Event {
208 entity_id: EntityId,
209 payload: Box<dyn Any>,
210 },
211 ModelNotification {
212 model_id: EntityId,
213 },
214 ViewNotification {
215 window_id: WindowId,
216 view_id: EntityId,
217 },
218 Focus {
219 window_id: WindowId,
220 view_id: EntityId,
221 },
222 TypedAction {
223 window_id: WindowId,
224 view_id: EntityId,
225 action: Box<dyn Action>,
226 },
227 GlobalAction {
228 name: &'static str,
229 location: &'static std::panic::Location<'static>,
230 arg: Box<dyn Any>,
231 },
232}
233 
234pub trait AnyView {
235 fn as_any(&self) -> &dyn Any;
236 fn as_any_mut(&mut self) -> &mut dyn Any;
237 fn ui_name(&self) -> &'static str;
238 fn render(&self, app: &AppContext) -> Box<dyn Element>;
239 fn on_focus(
240 &mut self,
241 focus_ctx: &FocusContext,
242 app: &mut AppContext,
243 window_id: WindowId,
244 view_id: EntityId,
245 );
246 fn on_blur(
247 &mut self,
248 blur_ctx: &BlurContext,
249 app: &mut AppContext,
250 window_id: WindowId,
251 view_id: EntityId,
252 );
253 fn keymap_context(&self, app: &AppContext) -> keymap::Context;
254 fn active_cursor_position(
255 &self,
256 app: &mut AppContext,
257 window_id: WindowId,
258 view_id: EntityId,
259 ) -> Option<CursorInfo>;
260 fn on_window_closed(&mut self, app: &mut AppContext, window_id: WindowId, view_id: EntityId);
261 fn on_window_transferred(
262 &mut self,
263 source_window_id: WindowId,
264 target_window_id: WindowId,
265 app: &mut AppContext,
266 view_id: EntityId,
267 );
268 fn self_or_child_interacted_with(
269 &self,
270 app: &mut AppContext,
271 window_id: WindowId,
272 view_id: EntityId,
273 );
274 fn accessibility_data(
275 &self,
276 app: &mut AppContext,
277 window_id: WindowId,
278 view_id: EntityId,
279 ) -> Option<AccessibilityData>;
280}
281 
282impl<T> AnyView for T
283where
284 T: View,
285{
286 fn as_any(&self) -> &dyn Any {
287 self
288 }
289 
290 fn as_any_mut(&mut self) -> &mut dyn Any {
291 self
292 }
293 
294 fn ui_name(&self) -> &'static str {
295 T::ui_name()
296 }
297 
298 fn render<'a>(&self, app: &AppContext) -> Box<dyn Element> {
299 View::render(self, app)
300 }
301 
302 fn active_cursor_position(
303 &self,
304 app: &mut AppContext,
305 window_id: WindowId,
306 view_id: EntityId,
307 ) -> Option<CursorInfo> {
308 let ctx = ViewContext::new(app, window_id, view_id);
309 View::active_cursor_position(self, &ctx)
310 }
311 
312 fn on_focus(
313 &mut self,
314 focus_ctx: &FocusContext,
315 app: &mut AppContext,
316 window_id: WindowId,
317 view_id: EntityId,
318 ) {
319 let mut ctx = ViewContext::new(app, window_id, view_id);
320 View::on_focus(self, focus_ctx, &mut ctx);
321 // Send notification to a11y tools that the view gained focus
322 if focus_ctx.is_self_focused() {
323 if let Some(accessibility_contents) = View::accessibility_contents(self, app) {
324 app.platform_delegate.set_accessibility_contents(
325 accessibility_contents.with_verbosity(app.a11y_verbosity),
326 );
327 }
328 }
329 }
330 
331 fn on_blur(
332 &mut self,
333 blur_ctx: &BlurContext,
334 app: &mut AppContext,
335 window_id: WindowId,
336 view_id: EntityId,
337 ) {
338 let mut ctx = ViewContext::new(app, window_id, view_id);
339 View::on_blur(self, blur_ctx, &mut ctx);
340 }
341 
342 fn on_window_closed(&mut self, app: &mut AppContext, window_id: WindowId, view_id: EntityId) {
343 let mut ctx = ViewContext::new(app, window_id, view_id);
344 View::on_window_closed(self, &mut ctx);
345 }
346 
347 fn on_window_transferred(
348 &mut self,
349 source_window_id: WindowId,
350 target_window_id: WindowId,
351 app: &mut AppContext,
352 view_id: EntityId,
353 ) {
354 let mut ctx = ViewContext::new(app, target_window_id, view_id);
355 View::on_window_transferred(self, source_window_id, target_window_id, &mut ctx);
356 }
357 
358 fn keymap_context(&self, app: &AppContext) -> keymap::Context {
359 View::keymap_context(self, app)
360 }
361 
362 fn self_or_child_interacted_with(
363 &self,
364 app: &mut AppContext,
365 window_id: WindowId,
366 view_id: EntityId,
367 ) {
368 let mut ctx = ViewContext::new(app, window_id, view_id);
369 View::self_or_child_interacted_with(self, &mut ctx)
370 }
371 
372 fn accessibility_data(
373 &self,
374 app: &mut AppContext,
375 window_id: WindowId,
376 view_id: EntityId,
377 ) -> Option<AccessibilityData> {
378 let mut ctx = ViewContext::new(app, window_id, view_id);
379 View::accessibility_data(self, &mut ctx)
380 }
381}
382 
383pub trait Handle<T> {
384 fn id(&self) -> EntityId;
385 fn location(&self) -> EntityLocation;
386}
387 
388#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
389pub enum EntityLocation {
390 Model(EntityId),
391 View(WindowId, EntityId),
392}
393 
394#[derive(Default)]
395struct RefCounts {
396 entity_counts: HashMap<EntityId, usize>,
397 dropped: DroppedItems,
398}
399 
400#[derive(Default)]
401struct DroppedItems {
402 models: HashSet<EntityId>,
403 views: HashSet<(WindowId, EntityId)>,
404}
405 
406impl RefCounts {
407 fn inc_entity(&mut self, entity_id: EntityId) {
408 *self.entity_counts.entry(entity_id).or_insert(0) += 1;
409 }
410 
411 fn dec_model(&mut self, model_id: EntityId) {
412 if let Some(count) = self.entity_counts.get_mut(&model_id) {
413 *count -= 1;
414 if *count == 0 {
415 self.entity_counts.remove(&model_id);
416 
417 self.dropped.models.insert(model_id);
418 }
419 } else {
420 panic!("Expected ref count to be positive")
421 }
422 }
423 
424 fn dec_view(&mut self, window_id: WindowId, view_id: EntityId) {
425 if let Some(count) = self.entity_counts.get_mut(&view_id) {
426 *count -= 1;
427 if *count == 0 {
428 self.entity_counts.remove(&view_id);
429 self.dropped.views.insert((window_id, view_id));
430 }
431 } else {
432 panic!("Expected ref count to be positive")
433 }
434 }
435 
436 fn take_dropped(&mut self) -> DroppedItems {
437 mem::take(&mut self.dropped)
438 }
439}
440 
441impl DroppedItems {
442 fn is_empty(&self) -> bool {
443 self.models.is_empty() && self.views.is_empty()
444 }
445}
446 
447type SubscriptionFromModelCallback = dyn FnMut(&mut dyn Any, &dyn Any, &mut AppContext, EntityId);
448type SubscriptionFromViewCallback =
449 dyn FnMut(&mut dyn Any, &dyn Any, &mut AppContext, WindowId, EntityId);
450type SubscriptionFromAppCallback = dyn FnMut(&dyn Any, &mut AppContext, EntityId);
451 
452/// Key that uniquely identifies a subscription for deferred unsubscribe tracking.
453#[derive(Hash, Eq, PartialEq, Clone, Copy)]
454pub(super) enum SubscriptionKey {
455 Model(EntityId),
456 View(WindowId, EntityId),
457}
458 
459/// Tracks pending unsubscribes during event emission.
460/// When `emit_event` is processing callbacks, unsubscribes are deferred to avoid
461/// O(N²) tombstone scanning. This struct collects the unsubscribes, which are then
462/// processed in a single pass at the end of event emission.
463pub(super) struct PendingUnsubscribes {
464 /// The entity we're currently emitting events for.
465 pub entity_id: EntityId,
466 /// Keys of subscriptions that should be removed after all callbacks complete.
467 pub keys: HashSet<SubscriptionKey>,
468}
469 
470/// Sources from where an [`Entity`] (e.g. View or Model) can be subscribed to for events.
471#[allow(clippy::enum_variant_names)]
472enum Subscription {
473 /// The [`Entity`] is subscribed to from a [`Model`].
474 FromModel {
475 model_id: EntityId,
476 callback: Box<SubscriptionFromModelCallback>,
477 },
478 /// The [`Entity`] is subscribed to from a [`View`].
479 FromView {
480 window_id: WindowId,
481 view_id: EntityId,
482 callback: Box<SubscriptionFromViewCallback>,
483 },
484 /// The [`Entity`] is subscribed to from the [`App`].
485 FromApp {
486 callback: Box<SubscriptionFromAppCallback>,
487 },
488}
489 
490impl Subscription {
491 /// Returns a key that uniquely identifies this subscription for deferred unsubscribe tracking.
492 /// Returns `None` for `FromApp` subscriptions since they cannot be unsubscribed.
493 fn subscription_key(&self) -> Option<SubscriptionKey> {
494 match self {
495 Subscription::FromModel { model_id, .. } => Some(SubscriptionKey::Model(*model_id)),
496 Subscription::FromView {
497 window_id, view_id, ..
498 } => Some(SubscriptionKey::View(*window_id, *view_id)),
499 Subscription::FromApp { .. } => None,
500 }
501 }
502}
503 
504type ObservationFromModelCallback = dyn FnMut(&mut dyn Any, EntityId, &mut AppContext, EntityId);
505type ObservationFromViewCallback =
506 dyn FnMut(&mut dyn Any, EntityId, &mut AppContext, WindowId, EntityId);
507type ObservationFromAppCallback = dyn FnMut(EntityId, &mut AppContext);
508 
509/// Sources from where an [`Entity`] can be observed for invalidations.
510#[allow(clippy::enum_variant_names)]
511enum Observation {
512 /// The [`Entity`] is observed from another [`Model`].
513 FromModel {
514 model_id: EntityId,
515 callback: Box<ObservationFromModelCallback>,
516 },
517 /// The [`Entity`] is observed from a [`View`].
518 FromView {
519 window_id: WindowId,
520 view_id: EntityId,
521 callback: Box<ObservationFromViewCallback>,
522 },
523 /// The [`Entity`] is observed from the [`App`].
524 FromApp {
525 callback: Box<ObservationFromAppCallback>,
526 },
527}
528 
529type ModelFromFutureCallback = dyn FnOnce(&mut dyn Any, Box<dyn Any>, &mut AppContext, EntityId);
530 
531type ModelFromStreamItemCallback = dyn FnMut(&mut dyn Any, Box<dyn Any>, &mut AppContext, EntityId);
532type ModelFromStreamDoneCallback = dyn FnOnce(&mut dyn Any, &mut AppContext, EntityId);
533 
534type ViewFromFutureCallback =
535 dyn FnOnce(&mut dyn AnyView, Box<dyn Any>, &mut AppContext, WindowId, EntityId);
536 
537type ViewFromStreamItemCallback =
538 dyn FnMut(&mut dyn AnyView, Box<dyn Any>, &mut AppContext, WindowId, EntityId);
539 
540type ViewFromStreamDoneCallback = dyn FnOnce(&mut dyn AnyView, &mut AppContext, WindowId, EntityId);
541 
542enum TaskCallback {
543 ModelFromFuture {
544 model_id: EntityId,
545 callback: Box<ModelFromFutureCallback>,
546 },
547 ModelFromStream {
548 model_id: EntityId,
549 on_item: Box<ModelFromStreamItemCallback>,
550 on_done: Box<ModelFromStreamDoneCallback>,
551 },
552 ViewFromFuture {
553 window_id: WindowId,
554 view_id: EntityId,
555 callback: Box<ViewFromFutureCallback>,
556 },
557 ViewFromStream {
558 window_id: WindowId,
559 view_id: EntityId,
560 on_item: Box<ViewFromStreamItemCallback>,
561 on_done: Box<ViewFromStreamDoneCallback>,
562 },
563}
564 
565/// Given a duration and a max jitter percentage, returns a duration representing the
566/// Duration + random value (0, jitter_percentage * Duration)
567pub fn duration_with_jitter(duration: Duration, max_jitter_percentage: f32) -> Duration {
568 let max_jitter = duration.mul_f32(max_jitter_percentage);
569 let jitter = max_jitter.mul_f32(rand::random());
570 duration + jitter
571}
572 
573/// Configurable retrying option for spawn_with_retry_on_error.
574#[derive(Clone, Copy, Debug)]
575pub struct RetryOption {
576 strategy: RetryStrategy,
577 /// Interval until the next retry.
578 interval: Duration,
579 /// The remaining number of retries left.
580 remaining_retry_count: usize,
581 /// The maximum jitter percentage to be added to the interval. If this is None, there's no jitter.
582 max_jitter_percentage: Option<f32>,
583}
584 
585impl RetryOption {
586 pub const fn linear(interval: Duration, remaining_retry_count: usize) -> Self {
587 Self {
588 strategy: RetryStrategy::LinearBackoff,
589 interval,
590 remaining_retry_count,
591 max_jitter_percentage: None,
592 }
593 }
594 
595 pub const fn exponential(
596 interval: Duration,
597 factor: f32,
598 remaining_retry_count: usize,
599 ) -> Self {
600 Self {
601 strategy: RetryStrategy::ExponentialBackoff(factor),
602 interval,
603 remaining_retry_count,
604 max_jitter_percentage: None,
605 }
606 }
607 
608 pub const fn with_jitter(mut self, max_jitter_percentage: f32) -> Self {
609 self.max_jitter_percentage = Some(max_jitter_percentage);
610 self
611 }
612 
613 /// Advance the retry option after receiving one failure callback.
614 pub fn advance(&mut self) {
615 self.remaining_retry_count = self.remaining_retry_count.saturating_sub(1);
616 
617 if let RetryStrategy::ExponentialBackoff(factor) = self.strategy {
618 self.interval = self.interval.mul_f32(factor);
619 }
620 }
621 
622 /// The number of remaining retries, not including previous attempts.
623 pub fn remaining_retries(&self) -> usize {
624 self.remaining_retry_count
625 }
626 
627 /// Computes the duration until the next retry.
628 pub fn duration(&self) -> Duration {
629 match self.max_jitter_percentage {
630 Some(max_jitter_percentage) => {
631 duration_with_jitter(self.interval, max_jitter_percentage)
632 }
633 None => self.interval,
634 }
635 }
636}
637 
638#[derive(Clone, Copy, Debug)]
639pub enum RetryStrategy {
640 /// Constant interval between each backoff.
641 LinearBackoff,
642 /// Exponential backoff with the set multiplication factor.
643 ExponentialBackoff(f32),
644}
645 
646/// State of the resolved future in `spawn_with_retry_on_error`.
647#[derive(Debug)]
648pub enum RequestState<T> {
649 /// Request succeeded with return value T.
650 RequestSucceeded(T),
651 /// Request failed but there are pending retries.
652 RequestFailedRetryPending(Error),
653 /// Request failed.
654 RequestFailed(Error),
655}
656 
657impl<T> RequestState<T> {
658 pub fn has_pending_retries(&self) -> bool {
659 matches!(self, RequestState::RequestFailedRetryPending(_))
660 }
661}
662 
663#[cfg(test)]
664#[path = "mod_test.rs"]
665mod tests;
666