1use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::collections::HashSet;
37use std::str::FromStr;
38
39pub mod error_types;
40pub mod future_views;
41pub mod security;
42
43pub use future_views::{HologramView, ParticleEmitter, StreamingText};
44
45pub mod asset;
47pub mod dependency;
48pub mod error_boundary;
49pub mod knowledge;
50pub mod renderer;
51pub mod undo;
52pub mod virtual_list;
53pub mod window;
54
55pub use asset::{AssetKey, AssetState, TokenValue, DesignTokens};
57pub use dependency::{DependencyGraph, FrameBudgetTracker, SubsystemBudget, InputLatencyTracker};
58pub use error_boundary::{ComponentErrorState, ErrorBoundary};
59pub use knowledge::{AnnouncementPriority, KnowledgeFragment, KnowledgeId, AppState, MemoryLayer, UiFidelityLevel, TemporalEdge, TemporalNode};
60pub use undo::{UndoGroup, UndoManager};
61pub use window::{Window, WindowCloseAction, WindowConfig, WindowHandle, WindowId, WindowLevel};
62
63pub trait View: Sized + Send {
64 type Body: View;
67
68 fn body(self) -> Self::Body;
69
70 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
73
74 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
77 Size::ZERO
78 }
79
80 fn layout(&self) -> Option<&dyn layout::LayoutView> {
82 None
83 }
84
85 fn flex_weight(&self) -> f32 {
87 0.0
88 }
89
90 fn get_grid_placement(&self) -> Option<GridPlacement> {
92 None
93 }
94
95 fn modifier<M: ViewModifier>(self, m: M) -> ModifiedView<Self, M> {
97 ModifiedView::new(self, m)
98 }
99
100 fn bifrost(
102 self,
103 blur: f32,
104 saturation: f32,
105 opacity: f32,
106 ) -> ModifiedView<Self, FrostedGlassModifier> {
107 self.modifier(FrostedGlassModifier {
108 blur,
109 saturation,
110 opacity,
111 fresnel_strength: 1.0,
112 })
113 }
114
115 fn bifrost_full(
117 self,
118 blur: f32,
119 saturation: f32,
120 opacity: f32,
121 fresnel_strength: f32,
122 ) -> ModifiedView<Self, FrostedGlassModifier> {
123 self.modifier(FrostedGlassModifier {
124 blur,
125 saturation,
126 opacity,
127 fresnel_strength,
128 })
129 }
130
131 fn gungnir(
133 self,
134 color: impl Into<String>,
135 radius: f32,
136 intensity: f32,
137 ) -> ModifiedView<Self, NeonGlowModifier> {
138 self.modifier(NeonGlowModifier {
139 color: color.into(),
140 radius,
141 intensity,
142 })
143 }
144
145 fn mjolnir_slice(self, angle: f32, offset: f32) -> ModifiedView<Self, GeometricClipModifier> {
147 self.modifier(GeometricClipModifier { angle, offset })
148 }
149
150 fn mjolnir_shatter(
152 self,
153 pieces: u32,
154 force: f32,
155 ) -> ModifiedView<Self, FragmentModifier> {
156 self.modifier(FragmentModifier { pieces, force })
157 }
158
159 fn bifrost_bridge(self, id: impl Into<String>) -> ModifiedView<Self, SharedElementModifier> {
161 self.modifier(SharedElementModifier { id: id.into() })
162 }
163
164 fn background(self, color: [f32; 4]) -> ModifiedView<Self, BackgroundModifier> {
166 self.modifier(BackgroundModifier { color })
167 }
168
169 fn padding(self, amount: f32) -> ModifiedView<Self, PaddingModifier> {
171 self.modifier(PaddingModifier { amount })
172 }
173
174 fn opacity(self, opacity: f32) -> ModifiedView<Self, OpacityModifier> {
176 self.modifier(OpacityModifier {
177 opacity: opacity.clamp(0.0, 1.0),
178 })
179 }
180
181 fn foreground_color(self, color: [f32; 4]) -> ModifiedView<Self, ForegroundColorModifier> {
183 self.modifier(ForegroundColorModifier { color })
184 }
185
186 fn frame(self, width: Option<f32>, height: Option<f32>) -> ModifiedView<Self, FrameModifier> {
189 self.modifier(FrameModifier {
190 width,
191 height,
192 min_width: None,
193 max_width: None,
194 min_height: None,
195 max_height: None,
196 alignment: Alignment::Center,
197 })
198 }
199
200 fn flex(self, weight: f32) -> ModifiedView<Self, FlexModifier> {
202 self.modifier(FlexModifier { weight })
203 }
204
205 fn grid_placement(self, placement: GridPlacement) -> ModifiedView<Self, GridPlacementModifier> {
207 self.modifier(GridPlacementModifier { placement })
208 }
209
210 fn overlay<O: View + Clone + 'static>(
212 self,
213 overlay: O,
214 alignment: Alignment,
215 offset: [f32; 2],
216 on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
217 ) -> ModifiedView<Self, OverlayModifier> {
218 self.modifier(OverlayModifier {
219 overlay: overlay.erase(),
220 alignment,
221 offset,
222 on_dismiss,
223 })
224 }
225
226 fn safe_area_padding(self) -> ModifiedView<Self, SafeAreaModifier> {
228 self.modifier(SafeAreaModifier { ignores: false })
229 }
230
231 fn ignores_safe_area(self) -> ModifiedView<Self, SafeAreaModifier> {
233 self.modifier(SafeAreaModifier { ignores: true })
234 }
235
236 fn clip_to_bounds(self) -> ModifiedView<Self, ClipModifier> {
238 self.modifier(ClipModifier)
239 }
240
241 fn border(self, color: [f32; 4], width: f32) -> ModifiedView<Self, BorderModifier> {
243 self.modifier(BorderModifier { color, width })
244 }
245
246 fn elevation(self, level: f32) -> ModifiedView<Self, ElevationModifier> {
248 self.modifier(ElevationModifier { level })
249 }
250
251 fn position(self, x: f32, y: f32) -> ModifiedView<Self, PositionModifier> {
254 self.modifier(PositionModifier { x, y })
255 }
256
257 fn z_index(self, z: i32) -> ModifiedView<Self, ZIndexModifier> {
260 self.modifier(ZIndexModifier { z_index: z })
261 }
262
263 fn magnetic(self, radius: f32, intensity: f32) -> ModifiedView<Self, MagneticPullModifier> {
265 self.modifier(MagneticPullModifier { radius, intensity })
266 }
267
268 fn mani_glow(self, color: [f32; 4], radius: f32) -> ModifiedView<Self, CursorGlowModifier> {
270 self.modifier(CursorGlowModifier { color, radius })
271 }
272
273 fn memory_layer(self, layer: MemoryLayer) -> ModifiedView<Self, MemoryLayerModifier> {
275 self.modifier(MemoryLayerModifier { layer })
276 }
277
278 fn fafnir_evolve(self, id: u64) -> ModifiedView<Self, EvolvingInteractionModifier> {
280 self.modifier(EvolvingInteractionModifier { id })
281 }
282
283 fn mimir_intent(self) -> ModifiedView<Self, IntentPredictionModifier> {
285 self.modifier(IntentPredictionModifier)
286 }
287
288 fn kvasir_vibes(self, complexity: f32) -> ModifiedView<Self, ComplexityTelemetryModifier> {
290 self.modifier(ComplexityTelemetryModifier { complexity })
291 }
292
293 fn odins_eye(self) -> ModifiedView<Self, ObservabilityOverlayModifier> {
295 self.modifier(ObservabilityOverlayModifier)
296 }
297
298 fn on_appear<F: Fn() + Send + Sync + 'static>(
300 self,
301 action: F,
302 ) -> ModifiedView<Self, LifecycleModifier> {
303 self.modifier(LifecycleModifier {
304 on_appear: Some(Arc::new(action)),
305 on_disappear: None,
306 })
307 }
308
309 fn on_disappear<F: Fn() + Send + Sync + 'static>(
311 self,
312 action: F,
313 ) -> ModifiedView<Self, LifecycleModifier> {
314 self.modifier(LifecycleModifier {
315 on_appear: None,
316 on_disappear: Some(Arc::new(action)),
317 })
318 }
319
320 fn on_click<F: Fn() + Send + Sync + 'static>(
322 self,
323 action: F,
324 ) -> ModifiedView<Self, OnClickModifier> {
325 self.modifier(OnClickModifier {
326 action: Arc::new(action),
327 })
328 }
329
330 fn on_pointer_enter<F: Fn() + Send + Sync + 'static>(
332 self,
333 action: F,
334 ) -> ModifiedView<Self, OnPointerEnterModifier> {
335 self.modifier(OnPointerEnterModifier {
336 action: Arc::new(action),
337 })
338 }
339
340 fn on_pointer_leave<F: Fn() + Send + Sync + 'static>(
342 self,
343 action: F,
344 ) -> ModifiedView<Self, OnPointerLeaveModifier> {
345 self.modifier(OnPointerLeaveModifier {
346 action: Arc::new(action),
347 })
348 }
349
350 fn on_pointer_move<F: Fn(f32, f32) + Send + Sync + 'static>(
352 self,
353 action: F,
354 ) -> ModifiedView<Self, OnPointerMoveModifier> {
355 self.modifier(OnPointerMoveModifier {
356 action: Arc::new(action),
357 })
358 }
359
360 fn on_pointer_down<F: Fn() + Send + Sync + 'static>(
362 self,
363 action: F,
364 ) -> ModifiedView<Self, OnPointerDownModifier> {
365 self.modifier(OnPointerDownModifier {
366 action: Arc::new(action),
367 })
368 }
369
370 fn on_pointer_up<F: Fn() + Send + Sync + 'static>(
372 self,
373 action: F,
374 ) -> ModifiedView<Self, OnPointerUpModifier> {
375 self.modifier(OnPointerUpModifier {
376 action: Arc::new(action),
377 })
378 }
379
380 fn erase(self) -> AnyView
382 where
383 Self: Clone + 'static,
384 {
385 AnyView::new(self)
386 }
387
388 fn aria_properties(&self) -> Option<AriaProperties> {
396 None
397 }
398
399 fn on_key_event(&self, _key: &str, _modifiers: KeyModifiers) -> bool {
402 false
403 }
404
405 fn key_shortcuts(&self) -> Vec<KeyShortcut> {
407 vec![]
408 }
409
410 fn changed(&self) -> bool { true }
414
415 fn view_id(&self) -> Option<u64> { None }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
427pub enum AriaRole {
428 Alert,
429 Alertdialog,
430 Article,
431 Banner,
432 Button,
433 Checkbox,
434 Columnheader,
435 Combobox,
436 Complementary,
437 Contentinfo,
438 Dialog,
439 Form,
440 Grid,
441 Gridcell,
442 Heading,
443 Img,
444 Link,
445 List,
446 Listbox,
447 Listitem,
448 Main,
449 Menu,
450 Menubar,
451 Menuitem,
452 Menuitemcheckbox,
453 Menuitemradio,
454 Navigation,
455 None,
456 Note,
457 Option,
458 Presentation,
459 Progressbar,
460 Radio,
461 Radiogroup,
462 Region,
463 Row,
464 Rowgroup,
465 Rowheader,
466 Search,
467 Separator,
468 Slider,
469 Spinbutton,
470 Status,
471 Switch,
472 Tab,
473 Table,
474 Tablist,
475 Tabpanel,
476 Textbox,
477 Toolbar,
478 Tooltip,
479 Tree,
480 Treeitem,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct AriaProperties {
486 pub role: AriaRole,
487 pub label: String,
488 pub description: Option<String>,
489 pub value: Option<String>,
490 pub pressed: Option<bool>,
491 pub checked: Option<bool>,
492 pub expanded: Option<bool>,
493 pub disabled: bool,
494 pub hidden: bool,
495 pub level: Option<u8>,
496 pub shortcut: Option<String>,
497 pub focused: bool,
498 pub live: Option<String>,
499 pub atomic: bool,
500}
501
502impl AriaProperties {
503 pub fn new(role: AriaRole, label: impl Into<String>) -> Self {
504 Self {
505 role,
506 label: label.into(),
507 description: None,
508 value: None,
509 pressed: None,
510 checked: None,
511 expanded: None,
512 disabled: false,
513 hidden: false,
514 level: None,
515 shortcut: None,
516 focused: false,
517 live: None,
518 atomic: false,
519 }
520 }
521
522 pub fn description(mut self, d: impl Into<String>) -> Self {
523 self.description = Some(d.into());
524 self
525 }
526 pub fn value(mut self, v: impl Into<String>) -> Self {
527 self.value = Some(v.into());
528 self
529 }
530 pub fn checked(mut self, c: bool) -> Self {
531 self.checked = Some(c);
532 self
533 }
534 pub fn disabled(mut self, d: bool) -> Self {
535 self.disabled = d;
536 self
537 }
538 pub fn expanded(mut self, e: bool) -> Self {
539 self.expanded = Some(e);
540 self
541 }
542 pub fn level(mut self, l: u8) -> Self {
543 self.level = Some(l.clamp(1, 6));
544 self
545 }
546 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
547 self.shortcut = Some(s.into());
548 self
549 }
550 pub fn focused(mut self, f: bool) -> Self {
551 self.focused = f;
552 self
553 }
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
562pub struct KeyModifiers {
563 pub shift: bool,
564 pub ctrl: bool,
565 pub alt: bool,
566 pub meta: bool,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct KeyShortcut {
572 pub key: String,
573 pub modifiers: KeyModifiers,
574 pub description: String,
575}
576
577impl KeyShortcut {
578 pub fn new(key: impl Into<String>, desc: impl Into<String>) -> Self {
579 Self {
580 key: key.into(),
581 modifiers: KeyModifiers::default(),
582 description: desc.into(),
583 }
584 }
585 pub fn with_ctrl(mut self) -> Self {
586 self.modifiers.ctrl = true;
587 self
588 }
589 pub fn with_shift(mut self) -> Self {
590 self.modifiers.shift = true;
591 self
592 }
593 pub fn with_alt(mut self) -> Self {
594 self.modifiers.alt = true;
595 self
596 }
597 pub fn with_meta(mut self) -> Self {
598 self.modifiers.meta = true;
599 self
600 }
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
609pub struct FocusableId(String);
610
611impl FocusableId {
612 pub fn as_str(&self) -> &str {
614 &self.0
615 }
616}
617
618impl From<&str> for FocusableId {
619 fn from(s: &str) -> Self {
620 Self(s.to_string())
621 }
622}
623impl From<String> for FocusableId {
624 fn from(s: String) -> Self {
625 Self(s)
626 }
627}
628
629#[derive(Debug, Clone)]
631pub struct FocusTrap {
632 pub id: FocusableId,
633 pub order: Vec<FocusableId>,
634 pub wrap: bool,
635}
636
637impl FocusTrap {
638 pub fn new(id: impl Into<FocusableId>, order: Vec<FocusableId>) -> Self {
639 Self {
640 id: id.into(),
641 order,
642 wrap: true,
643 }
644 }
645}
646
647#[derive(Debug, Default)]
649pub struct FocusManager {
650 order: Vec<FocusableId>,
651 order_set: HashSet<FocusableId>,
652 focused: Option<FocusableId>,
653 traps: Vec<FocusTrap>,
654}
655
656impl FocusManager {
657 pub fn new() -> Self {
658 Self::default()
659 }
660
661 pub fn register(&mut self, id: impl Into<FocusableId>) {
662 let id = id.into();
663 if self.order_set.insert(id.clone()) {
664 self.order.push(id);
665 }
666 }
667
668 pub fn unregister(&mut self, id: &FocusableId) {
669 if self.order_set.remove(id) {
670 self.order.retain(|x| x != id);
671 }
672 if self.focused.as_ref() == Some(id) {
673 self.focused = None;
674 }
675 }
676
677 pub fn focused(&self) -> Option<&FocusableId> {
678 self.focused.as_ref()
679 }
680
681 pub fn focus(&mut self, id: impl Into<FocusableId>) -> bool {
682 let id = id.into();
683 if self.order.contains(&id) || self.traps.iter().any(|t| t.order.contains(&id)) {
684 self.focused = Some(id);
685 true
686 } else {
687 false
688 }
689 }
690
691 pub fn focus_next(&mut self) -> Option<&FocusableId> {
692 let order = self.effective_order();
693 if order.is_empty() {
694 return None;
695 }
696 let idx = self
697 .focused
698 .as_ref()
699 .and_then(|f| order.iter().position(|x| x == f));
700 let next = match idx {
701 Some(i) if i + 1 < order.len() => &order[i + 1],
702 _ => &order[0],
703 };
704 self.focused = Some(next.clone());
705 self.focused.as_ref()
706 }
707
708 pub fn focus_prev(&mut self) -> Option<&FocusableId> {
709 let order = self.effective_order();
710 if order.is_empty() {
711 return None;
712 }
713 let idx = self
714 .focused
715 .as_ref()
716 .and_then(|f| order.iter().position(|x| x == f));
717 let prev = match idx {
718 Some(i) if i > 0 => &order[i - 1],
719 _ => &order[order.len() - 1],
720 };
721 self.focused = Some(prev.clone());
722 self.focused.as_ref()
723 }
724
725 pub fn push_trap(&mut self, trap: FocusTrap) -> FocusableId {
726 let id = trap.id.clone();
727 self.traps.push(trap);
728 id
729 }
730
731 pub fn pop_trap(&mut self) {
732 self.traps.pop();
733 }
734 pub fn trap_count(&self) -> usize {
735 self.traps.len()
736 }
737
738 fn effective_order(&self) -> &[FocusableId] {
739 self.traps
740 .last()
741 .map(|t| t.order.as_slice())
742 .unwrap_or(&self.order)
743 }
744}
745
746pub fn is_reduced_motion() -> bool {
755 AccessibilityPreferences::detect_from_system().reduce_motion
756}
757
758pub fn effective_duration(secs: f32) -> f32 {
760 if is_reduced_motion() { 0.0 } else { secs }
761}
762
763pub trait ErasedView: Send {
765 fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect);
766 fn name(&self) -> &'static str;
767 fn flex_weight_erased(&self) -> f32;
768 fn layout_erased(&self) -> Option<&dyn layout::LayoutView>;
769 fn grid_placement_erased(&self) -> Option<GridPlacement>;
770 fn clone_box(&self) -> Box<dyn ErasedView>;
771}
772
773impl<V: View + Clone + 'static> ErasedView for V {
774 fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect) {
775 self.render(renderer, rect);
776 }
777
778 fn name(&self) -> &'static str {
779 std::any::type_name::<V>()
780 }
781
782 fn flex_weight_erased(&self) -> f32 {
783 self.flex_weight()
784 }
785
786 fn layout_erased(&self) -> Option<&dyn layout::LayoutView> {
787 self.layout()
788 }
789
790 fn grid_placement_erased(&self) -> Option<GridPlacement> {
791 self.get_grid_placement()
792 }
793
794 fn clone_box(&self) -> Box<dyn ErasedView> {
795 Box::new(self.clone())
796 }
797}
798
799pub struct MemoView<V, F> {
802 id: u64,
803 data_hash: u64,
804 builder: F,
805 _v: std::marker::PhantomData<V>,
806}
807
808impl<V: View, F: Fn() -> V + Send + Sync> MemoView<V, F> {
809 pub fn new(id: u64, data_hash: u64, builder: F) -> Self {
811 Self {
812 id,
813 data_hash,
814 builder,
815 _v: std::marker::PhantomData,
816 }
817 }
818}
819
820impl<V: View + 'static, F: Fn() -> V + Send + Sync + 'static> View for MemoView<V, F> {
821 type Body = Never;
822 fn body(self) -> Self::Body {
823 unreachable!("MemoView does not have a body")
826 }
827
828 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
829 renderer.memoize(self.id, self.data_hash, &|r| {
830 let view = (self.builder)();
831 view.render(r, rect);
832 });
833 }
834}
835
836pub struct AnyView {
838 inner: Box<dyn ErasedView>,
839}
840
841impl Clone for AnyView {
842 fn clone(&self) -> Self {
843 Self {
844 inner: self.inner.clone_box(),
845 }
846 }
847}
848
849impl AnyView {
850 pub fn new<V: View + Clone + 'static>(view: V) -> Self {
851 Self {
852 inner: Box::new(view),
853 }
854 }
855}
856
857impl View for AnyView {
858 type Body = Never;
859 fn body(self) -> Self::Body {
860 unreachable!()
863 }
864
865 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
866 renderer.push_vnode(rect, self.inner.name());
867 self.inner.render_erased(renderer, rect);
868 renderer.pop_vnode();
869 }
870
871 fn flex_weight(&self) -> f32 {
872 self.inner.flex_weight_erased()
873 }
874
875 fn layout(&self) -> Option<&dyn layout::LayoutView> {
876 self.inner.layout_erased()
877 }
878
879 fn get_grid_placement(&self) -> Option<GridPlacement> {
880 self.inner.grid_placement_erased()
881 }
882}
883
884#[derive(Debug, Clone, PartialEq)]
888pub struct SharedElementModifier {
889 pub id: String,
890}
891
892impl ViewModifier for SharedElementModifier {
893 fn modify<V: View>(self, content: V) -> impl View {
894 ModifiedView::new(content, self)
895 }
896
897 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
898 renderer.register_shared_element(&self.id, rect);
900 }
901}
902
903#[derive(Debug, Clone, Copy, PartialEq)]
906pub struct GeometricClipModifier {
907 pub angle: f32,
908 pub offset: f32,
909}
910
911impl ViewModifier for GeometricClipModifier {
912 fn modify<V: View>(self, content: V) -> impl View {
913 ModifiedView::new(content, self)
914 }
915
916 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
917 renderer.push_mjolnir_slice(self.angle, self.offset);
918 }
919
920 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
921 renderer.pop_mjolnir_slice();
922 }
923}
924
925#[derive(Debug, Clone, Copy, PartialEq)]
928pub struct FragmentModifier {
929 pub pieces: u32,
930 pub force: f32,
931}
932
933impl ViewModifier for FragmentModifier {
934 fn modify<V: View>(self, content: V) -> impl View {
935 ModifiedView::new(content, self)
936 }
937
938 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
939 let pieces = self.pieces.max(1);
941 for i in 0..pieces {
942 let progress = i as f32 / pieces as f32;
943 let next_progress = (i + 1) as f32 / pieces as f32;
944
945 let angle_start = progress * 360.0;
946 let angle_end = next_progress * 360.0;
947
948 renderer.push_mjolnir_slice(angle_start, 0.0);
950 renderer.push_mjolnir_slice(angle_end + 180.0, 0.0);
951
952 let mid_angle = (angle_start + angle_end) / 2.0;
954 let rad = mid_angle.to_radians();
955 let dx = rad.cos() * self.force;
956 let dy = rad.sin() * self.force;
957
958 let shard_rect = Rect {
959 x: rect.x + dx,
960 y: rect.y + dy,
961 ..rect
962 };
963
964 view.render(renderer, shard_rect);
965
966 renderer.pop_mjolnir_slice();
967 renderer.pop_mjolnir_slice();
968 }
969 }
970}
971
972#[derive(Debug, Clone, Copy, PartialEq)]
975pub struct FrostedGlassModifier {
976 pub blur: f32,
977 pub saturation: f32,
978 pub opacity: f32,
979 pub fresnel_strength: f32,
981}
982
983impl ViewModifier for FrostedGlassModifier {
984 fn modify<V: View>(self, content: V) -> impl View {
985 ModifiedView::new(content, self)
986 }
987
988 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
989 if renderer.is_over_budget() {
990 renderer.bifrost(rect, self.blur * 0.5, self.saturation, self.opacity);
992 } else {
993 renderer.bifrost(rect, self.blur, self.saturation, self.opacity);
994 }
995 }
996}
997
998#[derive(Debug, Clone, Copy, PartialEq)]
1000pub struct BackgroundModifier {
1001 pub color: [f32; 4],
1002}
1003
1004impl ViewModifier for BackgroundModifier {
1005 fn modify<V: View>(self, content: V) -> impl View {
1006 ModifiedView::new(content, self)
1007 }
1008
1009 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1010 renderer.fill_rect(rect, self.color);
1011 }
1012}
1013
1014#[derive(Debug, Clone, Copy, PartialEq)]
1016pub struct PaddingModifier {
1017 pub amount: f32,
1018}
1019
1020impl ViewModifier for PaddingModifier {
1021 fn modify<V: View>(self, content: V) -> impl View {
1022 ModifiedView::new(content, self)
1023 }
1024
1025 fn transform_rect(&self, rect: Rect) -> Rect {
1026 Rect {
1027 x: rect.x + self.amount,
1028 y: rect.y + self.amount,
1029 width: (rect.width - 2.0 * self.amount).max(0.0),
1030 height: (rect.height - 2.0 * self.amount).max(0.0),
1031 }
1032 }
1033
1034 fn transform_proposal(&self, mut proposal: SizeProposal) -> SizeProposal {
1035 if let Some(w) = proposal.width {
1036 proposal.width = Some((w - 2.0 * self.amount).max(0.0));
1037 }
1038 if let Some(h) = proposal.height {
1039 proposal.height = Some((h - 2.0 * self.amount).max(0.0));
1040 }
1041 proposal
1042 }
1043
1044 fn transform_size(&self, mut size: Size) -> Size {
1045 size.width += 2.0 * self.amount;
1046 size.height += 2.0 * self.amount;
1047 size
1048 }
1049}
1050
1051#[derive(Debug, Clone, PartialEq)]
1054pub struct NeonGlowModifier {
1055 pub color: String,
1056 pub radius: f32,
1057 pub intensity: f32,
1058}
1059
1060impl ViewModifier for NeonGlowModifier {
1061 fn modify<V: View>(self, content: V) -> impl View {
1062 ModifiedView::new(content, self)
1063 }
1064
1065 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1066 renderer.stroke_rect(rect, [0.0, 1.0, 1.0, self.intensity], self.radius / 10.0);
1068 }
1069}
1070
1071#[derive(Debug, Clone, Copy, PartialEq)]
1073pub struct PulsingGlowModifier {
1074 pub color: [f32; 4],
1075 pub radius: f32,
1076 pub speed: f32,
1077}
1078
1079impl ViewModifier for PulsingGlowModifier {
1080 fn modify<V: View>(self, content: V) -> impl View {
1081 ModifiedView::new(content, self)
1082 }
1083
1084 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1085 let time = std::time::SystemTime::now()
1086 .duration_since(std::time::UNIX_EPOCH)
1087 .unwrap_or_default()
1088 .as_secs_f32();
1089
1090 let intensity = (time * self.speed).sin() * 0.5 + 0.5;
1093 let mut color = self.color;
1094 color[3] *= intensity;
1095
1096 renderer.stroke_rect(rect, color, self.radius);
1098 }
1099}
1100
1101#[derive(Debug, Clone, Copy, PartialEq)]
1103pub struct MagneticPullModifier {
1104 pub radius: f32,
1105 pub intensity: f32,
1106}
1107
1108impl ViewModifier for MagneticPullModifier {
1109 fn modify<V: View>(self, content: V) -> impl View {
1110 ModifiedView::new(content, self)
1111 }
1112
1113 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1114 let [px, py] = renderer.get_pointer_position();
1115 let center_x = rect.x + rect.width / 2.0;
1116 let center_y = rect.y + rect.height / 2.0;
1117
1118 let dx = px - center_x;
1119 let dy = py - center_y;
1120 let dist = (dx * dx + dy * dy).sqrt();
1121
1122 let mut offset_x = 0.0;
1123 let mut offset_y = 0.0;
1124
1125 if dist < self.radius && dist > 0.0 {
1126 let force = (1.0 - dist / self.radius) * self.intensity;
1127 offset_x = dx * force;
1128 offset_y = dy * force;
1129 }
1130
1131 let magnetic_rect = Rect {
1132 x: rect.x + offset_x,
1133 y: rect.y + offset_y,
1134 ..rect
1135 };
1136
1137 view.render(renderer, magnetic_rect);
1138 }
1139}
1140
1141#[derive(Debug, Clone, Copy, PartialEq)]
1144pub struct CursorGlowModifier {
1145 pub color: [f32; 4],
1146 pub radius: f32,
1147}
1148
1149impl ViewModifier for CursorGlowModifier {
1150 fn modify<V: View>(self, content: V) -> impl View {
1151 ModifiedView::new(content, self)
1152 }
1153
1154 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1155 if crate::load_system_state().realm == UiFidelityLevel::Asgard {
1156 renderer.mani_glow(rect, self.color, self.radius);
1157 }
1158 view.render(renderer, rect);
1159 }
1160}
1161
1162#[derive(Debug, Clone, Copy, PartialEq)]
1167pub struct MemoryLayerModifier {
1168 pub layer: MemoryLayer,
1169}
1170
1171impl ViewModifier for MemoryLayerModifier {
1172 fn modify<V: View>(self, content: V) -> impl View {
1173 ModifiedView::new(content, self)
1174 }
1175
1176 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1177 let realm = crate::load_system_state().realm;
1178 match self.layer {
1179 MemoryLayer::Episodic => {
1180 if realm == UiFidelityLevel::Asgard {
1181 renderer.bifrost(rect, 40.0, 1.2, 0.7);
1182 } else {
1183 renderer.fill_rect(rect, [0.1, 0.12, 0.15, 0.8]);
1184 }
1185 }
1186 MemoryLayer::Semantic => {
1187 if realm == UiFidelityLevel::Asgard {
1188 renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0, 0.6);
1189 } else {
1190 renderer.stroke_rect(rect, [0.4, 0.4, 0.4, 1.0], 1.5);
1191 }
1192 }
1193 MemoryLayer::Procedural => {
1194 renderer.fill_rect(rect, [0.05, 0.05, 0.07, 0.95]);
1195 let stroke_color = if realm == UiFidelityLevel::Asgard {
1196 [0.3, 0.3, 0.3, 1.0]
1197 } else {
1198 [0.2, 0.2, 0.2, 1.0]
1199 };
1200 renderer.stroke_rect(rect, stroke_color, 2.0);
1201 }
1202 }
1203 view.render(renderer, rect);
1204 }
1205}
1206
1207#[derive(Debug, Clone, Copy, PartialEq)]
1211pub struct EvolvingInteractionModifier {
1212 pub id: u64,
1214}
1215
1216impl ViewModifier for EvolvingInteractionModifier {
1217 fn modify<V: View>(self, content: V) -> impl View {
1218 ModifiedView::new(content, self)
1219 }
1220
1221 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1222 let state = crate::load_system_state();
1223 let vitality = state
1224 .get_component_state::<f32>(self.id)
1225 .map(|v| *v.read().unwrap())
1226 .unwrap_or(1.0);
1227
1228 let growth = (vitality - 1.0).clamp(0.0, 4.0);
1231 let scale = 1.0 + growth * 0.12;
1232 let glow_intensity = growth * 0.25;
1233
1234 let id = self.id;
1236 renderer.register_handler(
1237 "pointermove",
1238 std::sync::Arc::new(move |_| {
1239 crate::update_system_state(|s| {
1240 let mut s = s.clone();
1241 let v = s
1242 .get_component_state::<f32>(id)
1243 .map(|v| *v.read().unwrap())
1244 .unwrap_or(1.0);
1245 s.set_component_state(id, (v + 0.05).min(5.0)); s
1247 });
1248 }),
1249 );
1250
1251 if scale > 1.01 {
1252 renderer.push_transform([0.0, 0.0], [scale, scale], 0.0);
1253 }
1254
1255 if glow_intensity > 0.1 && state.realm == UiFidelityLevel::Asgard {
1256 renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0 * vitality, glow_intensity);
1257 }
1258
1259 view.render(renderer, rect);
1260
1261 if scale > 1.01 {
1262 renderer.pop_transform();
1263 }
1264 }
1265}
1266
1267#[derive(Debug, Clone, Copy, PartialEq)]
1269pub struct IntentPredictionModifier;
1270
1271impl ViewModifier for IntentPredictionModifier {
1272 fn modify<V: View>(self, content: V) -> impl View {
1273 ModifiedView::new(content, self)
1274 }
1275
1276 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1277 let state = crate::load_system_state();
1278 let pos = state.last_pointer_pos;
1279 let vel = state.pointer_velocity;
1280
1281 let center = [rect.x + rect.width / 2.0, rect.y + rect.height / 2.0];
1283 let dx = center[0] - pos[0];
1284 let dy = center[1] - pos[1];
1285
1286 let dot = vel[0] * dx + vel[1] * dy;
1288 let speed_sq = vel[0] * vel[0] + vel[1] * vel[1];
1289 let dist_sq = dx * dx + dy * dy;
1290
1291 if dot > 0.0 && dist_sq < 250.0 * 250.0 && speed_sq > 0.5 && state.realm == UiFidelityLevel::Asgard {
1292 let intent_strength = (dot / (speed_sq.sqrt() * dist_sq.sqrt())).clamp(0.0, 1.0);
1294 renderer.stroke_rect(rect, [0.0, 0.9, 1.0, 0.3 * intent_strength], 1.5);
1295 }
1296
1297 view.render(renderer, rect);
1298 }
1299}
1300
1301#[derive(Debug, Clone, Copy, PartialEq)]
1303pub struct ComplexityTelemetryModifier {
1304 pub complexity: f32,
1305}
1306
1307impl ViewModifier for ComplexityTelemetryModifier {
1308 fn modify<V: View>(self, content: V) -> impl View {
1309 ModifiedView::new(content, self)
1310 }
1311
1312 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1313 if crate::load_system_state().realm == UiFidelityLevel::Asgard {
1314 let t = renderer.elapsed_time();
1315 let c = self.complexity.clamp(0.0, 1.0);
1316
1317 let blur = 20.0 + c * 40.0;
1320 let turbulence_x = (t * (1.0 + c * 2.0)).sin() * 8.0 * c;
1321 let turbulence_y = (t * (0.8 + c * 1.5)).cos() * 5.0 * c;
1322 renderer.bifrost(
1323 rect.offset(turbulence_x, turbulence_y),
1324 blur,
1325 0.8 + c * 0.4,
1326 0.25,
1327 );
1328
1329 if c > 0.2 {
1331 let pulse = (t * (3.0 + c * 5.0)).sin().abs() * c;
1332 let color = [0.0, 0.9, 1.0, 0.4 * pulse]; renderer.gungnir(rect, color, 12.0 + c * 24.0, 0.6 * pulse);
1334 }
1335
1336 if c > 0.7 {
1338 let instability = (t * 15.0).cos().abs() * (c - 0.7) * 3.3;
1339 let warning_color = [1.0, 0.0, 0.4, 0.12 * instability];
1340 renderer.fill_rect(rect, warning_color);
1341 renderer.stroke_rect(rect, [1.0, 0.0, 0.2, 0.45 * instability], 1.8);
1342 }
1343 }
1344 view.render(renderer, rect);
1345 }
1346}
1347
1348#[derive(Debug, Clone, Copy, PartialEq)]
1350pub struct ObservabilityOverlayModifier;
1351
1352impl ViewModifier for ObservabilityOverlayModifier {
1353 fn modify<V: View>(self, content: V) -> impl View {
1354 ModifiedView::new(content, self)
1355 }
1356
1357 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1358 let state = crate::load_system_state();
1359 let t = renderer.elapsed_time();
1360
1361 view.render(renderer, rect);
1363
1364 if state.realm == UiFidelityLevel::Asgard {
1365 let eye_pulse = (t * 0.5).sin().abs() * 0.05;
1368 renderer.draw_radial_gradient(
1369 rect,
1370 [0.0, 0.6, 0.8, 0.08 + eye_pulse], [0.0, 0.0, 0.0, 0.0], );
1373
1374 let hugin_rect = Rect {
1376 x: rect.x + 20.0,
1377 y: rect.y + 40.0,
1378 width: 200.0,
1379 height: rect.height - 80.0,
1380 };
1381 renderer.draw_text(
1382 "HUGIN: THOUGHT",
1383 hugin_rect.x,
1384 hugin_rect.y,
1385 10.0,
1386 [0.0, 1.0, 1.0, 0.6],
1387 );
1388 for (i, thought) in state.thoughts.iter().rev().take(10).enumerate() {
1389 renderer.draw_text(
1390 thought,
1391 hugin_rect.x,
1392 hugin_rect.y + 20.0 + i as f32 * 14.0,
1393 9.0,
1394 [1.0, 1.0, 1.0, 0.4],
1395 );
1396 }
1397
1398 let munin_rect = Rect {
1400 x: rect.x + rect.width - 220.0,
1401 y: rect.y + 40.0,
1402 width: 200.0,
1403 height: rect.height - 80.0,
1404 };
1405 renderer.draw_text(
1406 "MUNIN: MEMORY",
1407 munin_rect.x,
1408 munin_rect.y,
1409 10.0,
1410 [1.0, 0.84, 0.0, 0.6],
1411 );
1412 for (i, node) in state.nodes.iter().take(10).enumerate() {
1413 let opacity = (node.weight.min(1.0)) * 0.5;
1414 renderer.draw_text(
1415 &node.id,
1416 munin_rect.x,
1417 munin_rect.y + 20.0 + i as f32 * 14.0,
1418 9.0,
1419 [1.0, 1.0, 1.0, opacity],
1420 );
1421 }
1422
1423 if let Some(focus_id) = &state.odin_focus {
1425 renderer.draw_text(
1427 &format!("EYE FOCUS: {}", focus_id),
1428 rect.x + rect.width / 2.0 - 50.0,
1429 rect.y + 20.0,
1430 12.0,
1431 [0.0, 1.0, 1.0, 0.8],
1432 );
1433
1434 renderer.gungnir(
1437 Rect {
1438 x: rect.x + rect.width / 2.0 - 1.0,
1439 y: rect.y,
1440 width: 2.0,
1441 height: rect.height,
1442 },
1443 [0.0, 1.0, 1.0, 1.0],
1444 20.0,
1445 0.4,
1446 );
1447 }
1448 }
1449 }
1450}
1451
1452#[derive(Debug, Clone, Copy, PartialEq)]
1454pub struct SpringParams {
1455 pub stiffness: f32,
1456 pub damping: f32,
1457 pub mass: f32,
1458}
1459
1460impl SpringParams {
1461 pub fn snappy() -> Self {
1462 Self {
1463 stiffness: 230.0,
1464 damping: 22.0,
1465 mass: 1.0,
1466 }
1467 }
1468 pub fn fluid() -> Self {
1469 Self {
1470 stiffness: 170.0,
1471 damping: 26.0,
1472 mass: 1.0,
1473 }
1474 }
1475 pub fn heavy() -> Self {
1476 Self {
1477 stiffness: 90.0,
1478 damping: 20.0,
1479 mass: 1.0,
1480 }
1481 }
1482 pub fn bouncy() -> Self {
1483 Self {
1484 stiffness: 190.0,
1485 damping: 14.0,
1486 mass: 1.0,
1487 }
1488 }
1489}
1490
1491impl Default for SpringParams {
1492 fn default() -> Self {
1493 Self::fluid()
1494 }
1495}
1496
1497#[derive(Debug, Clone, Copy, PartialEq)]
1498struct SolverState {
1499 x: f32,
1500 v: f32,
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq)]
1506pub struct SpringSolver {
1507 params: SpringParams,
1508 target: f32,
1509 state: SolverState,
1510}
1511
1512impl SpringSolver {
1513 pub fn new(params: SpringParams, target: f32, current: f32) -> Self {
1515 Self {
1516 params,
1517 target,
1518 state: SolverState { x: current, v: 0.0 },
1519 }
1520 }
1521
1522 pub fn tick(&mut self, dt: f32) -> f32 {
1524 if dt <= 0.0 {
1525 return self.state.x;
1526 }
1527
1528 let mut remaining = dt;
1530 let step = 1.0 / 120.0;
1531
1532 while remaining > 0.0 {
1533 let d = remaining.min(step);
1534 self.step(d);
1535 remaining -= d;
1536 }
1537
1538 self.state.x
1539 }
1540
1541 fn step(&mut self, dt: f32) {
1542 let a = self.evaluate(self.state, 0.0, SolverState { x: 0.0, v: 0.0 });
1543 let b = self.evaluate(self.state, dt * 0.5, a);
1544 let c = self.evaluate(self.state, dt * 0.5, b);
1545 let d = self.evaluate(self.state, dt, c);
1546
1547 let dxdt = 1.0 / 6.0 * (a.x + 2.0 * (b.x + c.x) + d.x);
1548 let dvdt = 1.0 / 6.0 * (a.v + 2.0 * (b.v + c.v) + d.v);
1549
1550 self.state.x += dxdt * dt;
1551 self.state.v += dvdt * dt;
1552 }
1553
1554 fn evaluate(&self, initial: SolverState, dt: f32, d: SolverState) -> SolverState {
1555 let state = SolverState {
1556 x: initial.x + d.x * dt,
1557 v: initial.v + d.v * dt,
1558 };
1559 let force =
1560 -self.params.stiffness * (state.x - self.target) - self.params.damping * state.v;
1561 let mass = self.params.mass.max(0.001);
1562 SolverState {
1563 x: state.v,
1564 v: force / mass,
1565 }
1566 }
1567
1568 pub fn is_settled(&self) -> bool {
1569 (self.state.x - self.target).abs() < 0.001 && self.state.v.abs() < 0.001
1570 }
1571
1572 pub fn set_target(&mut self, target: f32) {
1573 self.target = target;
1574 }
1575
1576 pub fn current_value(&self) -> f32 {
1577 self.state.x
1578 }
1579}
1580
1581#[derive(Debug, Clone, PartialEq)]
1583pub struct SpringAnimationModifier {
1584 pub id: u64,
1585 pub target: f32,
1586 pub params: SpringParams,
1587}
1588
1589impl ViewModifier for SpringAnimationModifier {
1590 fn modify<V: View>(self, content: V) -> impl View {
1591 ModifiedView::new(content, self)
1592 }
1593
1594 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1595 let state = load_system_state();
1596
1597 let solver_lock_opt = state.get_component_state::<SpringSolver>(self.id);
1599
1600 let current_val;
1601
1602 if let Some(lock) = solver_lock_opt {
1603 let mut solver = lock.write().unwrap();
1605 solver.set_target(self.target);
1606 current_val = solver.tick(renderer.delta_time());
1607
1608 if !solver.is_settled() {
1610 renderer.request_redraw();
1611 }
1612 } else {
1613 let solver = SpringSolver::new(
1615 self.params,
1616 self.target,
1617 self.target, );
1619
1620 get_system_state().rcu(|old| {
1622 let mut new_state = (**old).clone();
1623 new_state.set_component_state(self.id, solver);
1624 new_state
1625 });
1626
1627 current_val = self.target;
1628 }
1629
1630 renderer.push_transform([0.0, current_val], [1.0, 1.0], 0.0);
1632 view.render(renderer, rect);
1633 renderer.pop_transform();
1634 }
1635}
1636
1637#[derive(Debug, Clone, Copy, PartialEq)]
1640pub struct TransformModifier {
1641 pub translation: [f32; 2],
1642 pub scale: [f32; 2],
1643 pub rotation: f32,
1644}
1645
1646impl Default for TransformModifier {
1647 fn default() -> Self {
1648 Self::new()
1649 }
1650}
1651
1652impl TransformModifier {
1653 pub fn new() -> Self {
1654 Self {
1655 translation: [0.0, 0.0],
1656 scale: [1.0, 1.0],
1657 rotation: 0.0,
1658 }
1659 }
1660
1661 pub fn translate(mut self, x: f32, y: f32) -> Self {
1662 self.translation = [x, y];
1663 self
1664 }
1665
1666 pub fn scale(mut self, x: f32, y: f32) -> Self {
1667 self.scale = [x, y];
1668 self
1669 }
1670
1671 pub fn rotate(mut self, radians: f32) -> Self {
1672 self.rotation = radians;
1673 self
1674 }
1675}
1676
1677impl ViewModifier for TransformModifier {
1678 fn modify<V: View>(self, content: V) -> impl View {
1679 ModifiedView::new(content, self)
1680 }
1681
1682 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1683 renderer.push_transform(self.translation, self.scale, self.rotation);
1684 view.render(renderer, rect);
1685 renderer.pop_transform();
1686 }
1687}
1688
1689#[derive(Clone)]
1692pub struct LifecycleModifier {
1693 pub on_appear: Option<Arc<dyn Fn() + Send + Sync>>,
1694 pub on_disappear: Option<Arc<dyn Fn() + Send + Sync>>,
1695}
1696
1697impl ViewModifier for LifecycleModifier {
1698 fn modify<V: View>(self, content: V) -> impl View {
1699 ModifiedView::new(content, self)
1700 }
1701}
1702
1703#[derive(Debug, Clone, Copy, PartialEq)]
1706pub struct OpacityModifier {
1707 pub opacity: f32,
1708}
1709
1710impl ViewModifier for OpacityModifier {
1711 fn modify<V: View>(self, content: V) -> impl View {
1712 ModifiedView::new(content, self)
1713 }
1714
1715 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1716 renderer.push_opacity(self.opacity);
1717 }
1718
1719 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1720 renderer.pop_opacity();
1721 }
1722}
1723
1724#[derive(Clone)]
1726pub struct OnClickModifier {
1727 pub action: Arc<dyn Fn() + Send + Sync>,
1728}
1729
1730impl ViewModifier for OnClickModifier {
1731 fn modify<V: View>(self, content: V) -> impl View {
1732 ModifiedView::new(content, self)
1733 }
1734
1735 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1736 let action = self.action.clone();
1737 renderer.register_handler(
1738 "pointerclick",
1739 std::sync::Arc::new(move |event| {
1740 if let Event::PointerClick { .. } = event {
1741 (action)();
1742 }
1743 }),
1744 );
1745 }
1746}
1747
1748#[derive(Clone)]
1750pub struct OnPointerEnterModifier {
1751 pub action: Arc<dyn Fn() + Send + Sync>,
1752}
1753
1754impl ViewModifier for OnPointerEnterModifier {
1755 fn modify<V: View>(self, content: V) -> impl View {
1756 ModifiedView::new(content, self)
1757 }
1758
1759 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1760 let action = self.action.clone();
1761 renderer.register_handler(
1762 "pointerenter",
1763 std::sync::Arc::new(move |event| {
1764 if let Event::PointerEnter = event {
1765 (action)();
1766 }
1767 }),
1768 );
1769 }
1770}
1771
1772#[derive(Clone)]
1774pub struct OnPointerLeaveModifier {
1775 pub action: Arc<dyn Fn() + Send + Sync>,
1776}
1777
1778impl ViewModifier for OnPointerLeaveModifier {
1779 fn modify<V: View>(self, content: V) -> impl View {
1780 ModifiedView::new(content, self)
1781 }
1782
1783 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1784 let action = self.action.clone();
1785 renderer.register_handler(
1786 "pointerleave",
1787 std::sync::Arc::new(move |event| {
1788 if let Event::PointerLeave = event {
1789 (action)();
1790 }
1791 }),
1792 );
1793 }
1794}
1795
1796#[derive(Clone)]
1798pub struct OnPointerMoveModifier {
1799 pub action: Arc<dyn Fn(f32, f32) + Send + Sync>,
1800}
1801
1802impl ViewModifier for OnPointerMoveModifier {
1803 fn modify<V: View>(self, content: V) -> impl View {
1804 ModifiedView::new(content, self)
1805 }
1806
1807 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1808 let action = self.action.clone();
1809 renderer.register_handler(
1810 "pointermove",
1811 std::sync::Arc::new(move |event| {
1812 if let Event::PointerMove { x, y, .. } = event {
1813 (action)(x, y);
1814 }
1815 }),
1816 );
1817 }
1818}
1819
1820#[derive(Clone)]
1822pub struct OnPointerDownModifier {
1823 pub action: Arc<dyn Fn() + Send + Sync>,
1824}
1825
1826impl ViewModifier for OnPointerDownModifier {
1827 fn modify<V: View>(self, content: V) -> impl View {
1828 ModifiedView::new(content, self)
1829 }
1830
1831 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1832 let action = self.action.clone();
1833 renderer.register_handler(
1834 "pointerdown",
1835 std::sync::Arc::new(move |event| {
1836 if let Event::PointerDown { .. } = event {
1837 (action)();
1838 }
1839 }),
1840 );
1841 }
1842}
1843
1844#[derive(Clone)]
1846pub struct OnPointerUpModifier {
1847 pub action: Arc<dyn Fn() + Send + Sync>,
1848}
1849
1850impl ViewModifier for OnPointerUpModifier {
1851 fn modify<V: View>(self, content: V) -> impl View {
1852 ModifiedView::new(content, self)
1853 }
1854
1855 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1856 let action = self.action.clone();
1857 renderer.register_handler(
1858 "pointerup",
1859 std::sync::Arc::new(move |event| {
1860 if let Event::PointerUp { .. } = event {
1861 (action)();
1862 }
1863 }),
1864 );
1865 }
1866}
1867
1868#[derive(Debug, Clone, Copy, PartialEq)]
1871pub struct ForegroundColorModifier {
1872 pub color: [f32; 4],
1873}
1874
1875impl ViewModifier for ForegroundColorModifier {
1876 fn modify<V: View>(self, content: V) -> impl View {
1877 ModifiedView::new(content, self)
1878 }
1879}
1880
1881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1884pub struct ClipModifier;
1885
1886impl ViewModifier for ClipModifier {
1887 fn modify<V: View>(self, content: V) -> impl View {
1888 ModifiedView::new(content, self)
1889 }
1890
1891 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1892 renderer.push_clip_rect(rect);
1893 }
1894
1895 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1896 renderer.pop_clip_rect();
1897 }
1898}
1899
1900#[derive(Debug, Clone, Copy, PartialEq)]
1902pub struct BorderModifier {
1903 pub color: [f32; 4],
1904 pub width: f32,
1905}
1906
1907impl ViewModifier for BorderModifier {
1908 fn modify<V: View>(self, content: V) -> impl View {
1909 ModifiedView::new(content, self)
1910 }
1911
1912 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1913 renderer.stroke_rect(rect, self.color, self.width);
1914 }
1915}
1916
1917#[doc(hidden)]
1919pub enum Never {}
1920
1921impl View for Never {
1922 type Body = Never;
1923 fn body(self) -> Never {
1924 unreachable!()
1927 }
1928}
1929
1930#[derive(Debug, Clone, Copy, Default)]
1932pub struct EmptyView;
1933
1934impl View for EmptyView {
1935 type Body = Never;
1936 fn body(self) -> Self::Body {
1937 unreachable!()
1940 }
1941 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1942 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
1943 Size {
1944 width: 0.0,
1945 height: 0.0,
1946 }
1947 }
1948}
1949
1950#[derive(Clone)]
1953pub struct ModifiedView<V, M> {
1954 view: V,
1955 modifier: M,
1956}
1957
1958impl<V: View, M: ViewModifier> ModifiedView<V, M> {
1959 #[doc(hidden)]
1960 pub fn new(view: V, modifier: M) -> Self {
1961 Self { view, modifier }
1962 }
1963}
1964
1965impl<V: View, M: ViewModifier> View for ModifiedView<V, M> {
1966 type Body = ModifiedView<V::Body, M>;
1967
1968 fn body(self) -> Self::Body {
1969 ModifiedView {
1970 view: self.view.body(),
1971 modifier: self.modifier.clone(),
1972 }
1973 }
1974
1975 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1976 self.modifier.render_view(&self.view, renderer, rect);
1977 }
1978
1979 fn intrinsic_size(&self, renderer: &mut dyn Renderer, proposal: SizeProposal) -> Size {
1980 self.modifier.measure_view(&self.view, renderer, proposal)
1981 }
1982
1983 fn flex_weight(&self) -> f32 {
1984 self.modifier.child_flex_weight(&self.view)
1985 }
1986
1987 fn layout(&self) -> Option<&dyn layout::LayoutView> {
1988 self.modifier.layout().or_else(|| self.view.layout())
1989 }
1990
1991 fn get_grid_placement(&self) -> Option<GridPlacement> {
1992 self.modifier
1993 .get_grid_placement()
1994 .or_else(|| self.view.get_grid_placement())
1995 }
1996}
1997
1998pub trait ViewModifier: Send + Clone {
1999 fn modify<V: View>(self, content: V) -> impl View;
2000
2001 fn get_grid_placement(&self) -> Option<GridPlacement> {
2003 None
2004 }
2005
2006 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
2008
2009 fn post_render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
2011
2012 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
2015 self.render(renderer, rect);
2016 let child_rect = self.transform_rect(rect);
2017 view.render(renderer, child_rect);
2018 self.post_render(renderer, rect);
2019 }
2020
2021 fn transform_rect(&self, rect: Rect) -> Rect {
2022 rect
2023 }
2024
2025 fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
2027 proposal
2028 }
2029
2030 fn transform_size(&self, size: Size) -> Size {
2032 size
2033 }
2034
2035 fn measure_view<V: View>(
2037 &self,
2038 view: &V,
2039 renderer: &mut dyn Renderer,
2040 proposal: SizeProposal,
2041 ) -> Size {
2042 let child_proposal = self.transform_proposal(proposal);
2043 let child_size = view.intrinsic_size(renderer, child_proposal);
2044 self.transform_size(child_size)
2045 }
2046
2047 fn child_flex_weight<V: View>(&self, view: &V) -> f32 {
2049 view.flex_weight()
2050 }
2051
2052 fn layout(&self) -> Option<&dyn layout::LayoutView> {
2053 None
2054 }
2055}
2056
2057#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2068pub struct RenderStateSnapshot {
2069 pub clip_depth: u32,
2070 pub opacity_depth: u32,
2071 pub slice_depth: u32,
2072 pub shadow_depth: u32,
2073 pub transform_depth: u32,
2074 pub vnode_depth: u32,
2075}
2076
2077#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
2079pub struct TelemetryData {
2080 pub frame_time_ms: f32,
2081 pub frame_budget_ms: f32,
2083 pub frame_budget_remaining_ms: f32,
2085 pub layout_budget_remaining_ms: f32,
2087 pub frame_over_budget: bool,
2089 pub layout_over_budget: bool,
2091 pub p99_frame_time_ms: f32,
2093 pub frame_jitter_ms: f32,
2095 pub hardware_stall_detected: bool,
2097
2098 pub input_time_ms: f32,
2100 pub state_flush_time_ms: f32,
2101 pub layout_time_ms: f32,
2102 pub draw_time_ms: f32,
2103 pub gpu_submit_time_ms: f32,
2104
2105 pub draw_calls: u32,
2106 pub vertices: u32,
2107
2108 pub berserker_rage: f32,
2110
2111 pub vram_usage_mb: f32,
2113 pub vram_textures_mb: f32,
2114 pub vram_buffers_mb: f32,
2115 pub vram_pipelines_mb: f32,
2116 pub vram_exhausted: bool,
2118}
2119
2120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2122pub struct FrameBudget {
2123 pub target_ms: f32,
2125 pub allow_degradation: bool,
2128}
2129
2130impl Default for FrameBudget {
2131 fn default() -> Self {
2132 Self {
2133 target_ms: 16.0,
2134 allow_degradation: true,
2135 }
2136 }
2137}
2138
2139pub trait ElapsedTime {
2147 fn elapsed_time(&self) -> f32;
2149
2150 fn delta_time(&self) -> f32;
2152}
2153
2154pub trait Renderer: ElapsedTime + Send {
2166 fn request_redraw(&mut self) {}
2169
2170 fn is_over_budget(&self) -> bool {
2173 false
2174 }
2175
2176 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]);
2178 fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]);
2179 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]);
2181
2182 fn draw_background_image(&mut self, image_name: &str, rect: Rect) {
2187 Renderer::draw_image(self, image_name, rect);
2188 }
2189
2190 fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
2194 let _ = (rect, radius, blur_radius);
2196 }
2197 fn fill_glass_rect_with_intensity(&mut self, rect: Rect, radius: f32, blur_radius: f32, glass_intensity: f32) {
2200 let _ = (rect, radius, blur_radius, glass_intensity);
2201 }
2202 fn fill_glass_rect_with_tint(&mut self, rect: Rect, radius: f32, blur_radius: f32, tint_color: [f32; 4], glass_intensity: f32) {
2205 let _ = (rect, radius, blur_radius, tint_color, glass_intensity);
2207 }
2208 fn fill_glass_rect_with_pressure(&mut self, rect: Rect, radius: f32, blur_radius: f32, pressure: f32) {
2213 Renderer::fill_glass_rect_with_intensity(self, rect, radius, blur_radius, pressure);
2215 }
2216
2217 fn fill_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4]) {
2220 Renderer::fill_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color);
2222 }
2223
2224 fn stroke_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4], stroke_width: f32) {
2226 Renderer::stroke_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color, stroke_width);
2227 }
2228
2229 fn draw_focus_ring(&mut self, rect: Rect, radius: f32, offset: f32, width: f32, color: [f32; 4]) {
2232 let ring_rect = Rect {
2234 x: rect.x - offset,
2235 y: rect.y - offset,
2236 width: rect.width + 2.0 * offset,
2237 height: rect.height + 2.0 * offset,
2238 };
2239 Renderer::stroke_rounded_rect(self, ring_rect, radius + offset, color, width);
2240 }
2241
2242 fn draw_3d_cube(&mut self, _rect: Rect, _color: [f32; 4], _rotation: [f32; 3]) {}
2245
2246 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2248 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32);
2249 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2251 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32);
2253 fn fill_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4]) {}
2255 fn stroke_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4], _stroke_width: f32) {}
2257
2258 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
2260 let span = cvkg_runic_text::TextSpan::new(
2261 text,
2262 cvkg_runic_text::TextStyle {
2263 family: "Inter".to_string(),
2264 font_size: size,
2265 color: [(color[0]*255.0) as u8, (color[1]*255.0) as u8, (color[2]*255.0) as u8, (color[3]*255.0) as u8],
2266 fallback_families: vec![
2267 "SF Pro".to_string(),
2268 "SF Pro Text".to_string(),
2269 "Helvetica Neue".to_string(),
2270 "Helvetica".to_string(),
2271 "Arial".to_string(),
2272 "sans-serif".to_string(),
2273 ],
2274 ..Default::default()
2275 },
2276 );
2277 if let Some(shaped) = Renderer::shape_rich_text(
2278 self,
2279 &[span],
2280 None,
2281 cvkg_runic_text::TextAlign::Start,
2282 cvkg_runic_text::TextOverflow::Visible,
2283 ) {
2284 Renderer::draw_shaped_text(self, &shaped, x, y);
2285 }
2286 }
2287
2288 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2290 let span = cvkg_runic_text::TextSpan::new(
2291 text,
2292 cvkg_runic_text::TextStyle {
2293 family: "Inter".to_string(),
2294 font_size: size,
2295 fallback_families: vec![
2296 "SF Pro".to_string(),
2297 "SF Pro Text".to_string(),
2298 "Helvetica Neue".to_string(),
2299 "Helvetica".to_string(),
2300 "Arial".to_string(),
2301 "sans-serif".to_string(),
2302 ],
2303 ..Default::default()
2304 },
2305 );
2306 if let Some(shaped) = Renderer::shape_rich_text(
2307 self,
2308 &[span],
2309 None,
2310 cvkg_runic_text::TextAlign::Start,
2311 cvkg_runic_text::TextOverflow::Visible,
2312 ) {
2313 let scale = self.text_scale_factor().max(1.0);
2314 (shaped.width / scale, shaped.height / scale)
2315 } else {
2316 (0.0, 0.0)
2317 }
2318 }
2319
2320 fn measure_text_baseline(&mut self, text: &str, size: f32) -> f32 {
2324 let span = cvkg_runic_text::TextSpan::new(
2325 text,
2326 cvkg_runic_text::TextStyle {
2327 family: "Inter".to_string(),
2328 font_size: size,
2329 fallback_families: vec![
2330 "SF Pro".to_string(),
2331 "SF Pro Text".to_string(),
2332 "Helvetica Neue".to_string(),
2333 "Helvetica".to_string(),
2334 "Arial".to_string(),
2335 "sans-serif".to_string(),
2336 ],
2337 ..Default::default()
2338 },
2339 );
2340 if let Some(shaped) = Renderer::shape_rich_text(
2341 self,
2342 &[span],
2343 None,
2344 cvkg_runic_text::TextAlign::Start,
2345 cvkg_runic_text::TextOverflow::Visible,
2346 ) {
2347 shaped.ascent / self.text_scale_factor().max(1.0)
2348 } else {
2349 0.0
2350 }
2351 }
2352
2353 fn text_scale_factor(&self) -> f32 {
2358 1.0
2359 }
2360
2361 fn shape_rich_text(
2362 &mut self,
2363 _spans: &[cvkg_runic_text::TextSpan],
2364 _max_width: Option<f32>,
2365 _align: cvkg_runic_text::TextAlign,
2366 _overflow: cvkg_runic_text::TextOverflow,
2367 ) -> Option<cvkg_runic_text::ShapedText> {
2368 None
2369 }
2370
2371 fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {}
2372
2373 fn draw_texture(&mut self, _texture_id: u32, _rect: Rect) {}
2376 fn draw_image(&mut self, _image_name: &str, _rect: Rect) {}
2378 fn load_image(&mut self, _name: &str, _data: &[u8]) {}
2380 fn prewarm_vram(&mut self, _assets: Vec<(String, Vec<u8>)>) {}
2383
2384 fn get_pointer_position(&self) -> [f32; 2] {
2386 [0.0, 0.0]
2387 }
2388
2389 fn upload_data_texture(&mut self, _id: &str, _data: &[f32], _width: u32, _height: u32) {}
2392 fn draw_heatmap(&mut self, _texture_id: &str, _rect: Rect, _palette: &str) {}
2394
2395 fn draw_mesh(&mut self, _mesh: &Mesh, _color: [f32; 4], _transform: glam::Mat4) {}
2398
2399 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
2401
2402 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
2405
2406 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
2409
2410 fn pop_transform_3d(&mut self) {}
2412
2413 fn render_scene_node_3d(
2422 &mut self,
2423 _position: [f32; 3],
2424 _rotation: [f32; 4],
2425 _scale: [f32; 3],
2426 _color: [f32; 4],
2427 _meshes: &[Mesh],
2428 ) {
2429 }
2431
2432 fn draw_linear_gradient(
2434 &mut self,
2435 _rect: Rect,
2436 _start_color: [f32; 4],
2437 _end_color: [f32; 4],
2438 _angle: f32,
2439 ) {
2440 }
2441 fn draw_radial_gradient(
2443 &mut self,
2444 _rect: Rect,
2445 _inner_color: [f32; 4],
2446 _outer_color: [f32; 4],
2447 ) {
2448 }
2449 fn draw_drop_shadow(
2451 &mut self,
2452 _rect: Rect,
2453 _radius: f32,
2454 _color: [f32; 4],
2455 _blur: f32,
2456 _spread: f32,
2457 ) {
2458 }
2459 fn stroke_dashed_rounded_rect(
2461 &mut self,
2462 _rect: Rect,
2463 _radius: f32,
2464 _color: [f32; 4],
2465 _width: f32,
2466 _dash: f32,
2467 _gap: f32,
2468 ) {
2469 }
2470 fn draw_9slice(
2472 &mut self,
2473 _image_name: &str,
2474 _rect: Rect,
2475 _left: f32,
2476 _top: f32,
2477 _right: f32,
2478 _bottom: f32,
2479 ) {
2480 }
2481
2482 fn push_clip_rect(&mut self, _rect: Rect) {}
2486 fn pop_clip_rect(&mut self) {}
2488 fn current_clip_rect(&self) -> Rect {
2491 Rect::new(-10000.0, -10000.0, 20000.0, 20000.0)
2492 }
2493
2494 fn push_opacity(&mut self, _opacity: f32) {}
2498 fn pop_opacity(&mut self) {}
2500
2501 fn set_theme(&mut self, _theme: ColorTheme) {}
2503 fn set_rage(&mut self, _rage: f32) {}
2504 fn set_berserker_mode(&mut self, _state: RenderIntensityMode) {}
2505 fn trigger_shatter_event(&mut self, _origin: [f32; 2], _force: f32) {}
2506 fn set_fireball_pos(&mut self, _pos: [f32; 2]) {}
2508 fn set_scene(&mut self, _scene: &str) {}
2510 fn set_scene_by_name(&mut self, name: &str) {
2514 if let Some(preset) = resolve_scene_by_name(name) {
2515 Renderer::set_scene_preset(self, preset);
2516 }
2517 }
2518
2519 fn capture_png(&mut self) -> Vec<u8> {
2522 Vec::new()
2523 }
2524 fn print(&mut self) {}
2526
2527 fn set_scene_preset(&mut self, _preset: u32) {}
2528
2529 fn bifrost(&mut self, _rect: Rect, _blur: f32, _saturation: f32, _opacity: f32) {}
2532 fn gungnir(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2534 fn gungnir_soft(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2536 fn set_default_background_color(&mut self, _color: [f32; 4]) {}
2539 fn mani_glow(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32) {}
2541 fn push_mjolnir_slice(&mut self, _angle: f32, _offset: f32) {}
2543 fn pop_mjolnir_slice(&mut self) {}
2544 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer));
2548 fn snapshot_render_state(&self) -> RenderStateSnapshot {
2553 RenderStateSnapshot::default()
2554 }
2555 fn restore_render_state(&mut self, _snap: RenderStateSnapshot) {}
2562 fn mjolnir_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2564 fn mjolnir_fluid_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2565 fn draw_mjolnir_bolt(&mut self, _from: [f32; 2], _to: [f32; 2], _color: [f32; 4]) {}
2566
2567 fn dispatch_particles(
2570 &mut self,
2571 _origin: [f32; 2],
2572 _count: u32,
2573 _effect_type: &str,
2574 _color: [f32; 4],
2575 ) {
2576 }
2577
2578 fn draw_hologram(&mut self, _rect: Rect, _hologram_id: &str, _time: f32) {}
2580
2581 fn set_aria_role(&mut self, _role: &str) {}
2583 fn set_aria_label(&mut self, _label: &str) {}
2584 fn set_aria_valuemin(&mut self, _min: f32) {}
2585 fn set_aria_valuemax(&mut self, _max: f32) {}
2586 fn set_aria_valuenow(&mut self, _now: f32) {}
2587
2588 fn push_focus_trap(&mut self, _element_id: &str) -> u64 { 0 }
2592
2593 fn pop_focus_trap(&mut self, _trap_id: u64) {}
2595
2596 fn register_shared_element(&mut self, _id: &str, _rect: Rect) {}
2598
2599 fn set_key(&mut self, _key: &str) {}
2601
2602 fn get_telemetry(&self) -> TelemetryData {
2605 TelemetryData::default()
2606 }
2607
2608 fn push_shadow(&mut self, _radius: f32, _color: [f32; 4], _offset: [f32; 2]) {}
2611 fn pop_shadow(&mut self) {}
2613
2614 fn push_vnode(&mut self, _rect: Rect, _name: &'static str) {}
2617 fn pop_vnode(&mut self) {}
2619 fn register_handler(
2621 &mut self,
2622 _event_type: &str,
2623 _handler: std::sync::Arc<dyn Fn(Event) + Send + Sync>,
2624 ) {
2625 }
2626
2627 fn set_z_index(&mut self, _z: f32) {}
2631 fn get_z_index(&self) -> f32 {
2633 0.0
2634 }
2635
2636 fn load_svg(&mut self, _name: &str, _svg_data: &[u8]) {}
2639 fn draw_svg(&mut self, _name: &str, _rect: Rect) {}
2641 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, _animation_time_offset: f32) {
2645 Renderer::draw_svg(self, name, rect);
2646 }
2647 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, _draw_order: i32) {
2650 Renderer::draw_svg(self, name, rect);
2651 }
2652 fn serialize_svg(&mut self, _name: &str) -> Result<String, String> {
2656 Err("SVG serialization not supported by this renderer".into())
2657 }
2658 fn apply_svg_filter(
2662 &mut self,
2663 _name: &str,
2664 _filter_id: &str,
2665 _region: Rect,
2666 ) -> Result<String, String> {
2667 Err("SVG filter not supported by this renderer".into())
2668 }
2669
2670 fn push_transform(&mut self, _translation: [f32; 2], _scale: [f32; 2], _rotation: f32) {}
2675 fn push_affine(&mut self, _transform: [f32; 6]) {}
2678 fn pop_transform(&mut self) {}
2680 fn query_layout(&self, _node_id: scene_graph::NodeId) -> Option<Rect> {
2682 None
2683 }
2684 fn set_debug_layout(&mut self, _enabled: bool) {}
2686 fn get_debug_layout(&self) -> bool {
2688 false
2689 }
2690
2691 fn set_material(&mut self, _material: crate::material::DrawMaterial) {}
2695 fn current_material(&self) -> crate::material::DrawMaterial {
2697 crate::material::DrawMaterial::Opaque
2698 }
2699
2700 fn mimir_intent(&self) -> [f32; 2] {
2703 [0.0, 0.0]
2704 }
2705 fn magnetic_warp(&self, pointer: [f32; 2], anchor_rect: Rect, strength: f32) -> [f32; 2] {
2707 if strength <= 0.0 {
2708 return pointer;
2709 }
2710 let cx = anchor_rect.x + anchor_rect.width / 2.0;
2711 let cy = anchor_rect.y + anchor_rect.height / 2.0;
2712 let dx = pointer[0] - cx;
2713 let dy = pointer[1] - cy;
2714 let dist = (dx * dx + dy * dy).sqrt();
2715 let radius = 120.0;
2716 if dist < radius && dist > 0.0 {
2717 let force = (1.0 - dist / radius) * strength;
2718 [pointer[0] - dx * force, pointer[1] - dy * force]
2719 } else {
2720 pointer
2721 }
2722 }
2723 fn mani_glow_intensity(&self, pointer: [f32; 2], bounds: Rect, radius: f32) -> f32 {
2725 let cx = bounds.x + bounds.width / 2.0;
2726 let cy = bounds.y + bounds.height / 2.0;
2727 let dist = ((pointer[0] - cx).powi(2) + (pointer[1] - cy).powi(2)).sqrt();
2728 if dist < radius {
2729 (1.0 - dist / radius).clamp(0.0, 1.0)
2730 } else {
2731 0.0
2732 }
2733 }
2734 fn fafnir_evolve(&self, pointer: [f32; 2], bounds: Rect, max_scale: f32) -> f32 {
2736 let prox = self.mani_glow_intensity(pointer, bounds, 120.0);
2737 1.0 + (max_scale - 1.0) * prox
2738 }
2739 fn set_sdf_shape(&mut self, _shape: crate::layout::SdfShape) {}
2741
2742 fn enter_portal(&mut self, _z_index: i32) {}
2752
2753 fn exit_portal(&mut self) {}
2757
2758 fn viewport_size(&self) -> Rect {
2761 Rect::new(0.0, 0.0, 1920.0, 1080.0)
2762 }
2763
2764 fn announce(&mut self, _message: &str, _priority: AnnouncementPriority) {}
2770}
2771
2772pub mod accessibility {
2774 pub fn relative_luminance(color: [f32; 4]) -> f32 {
2776 let f = |c: f32| {
2777 if c <= 0.03928 {
2778 c / 12.92
2779 } else {
2780 ((c + 0.055) / 1.055).powf(2.4)
2781 }
2782 };
2783 0.2126 * f(color[0]) + 0.7152 * f(color[1]) + 0.0722 * f(color[2])
2784 }
2785
2786 pub fn contrast_ratio(c1: [f32; 4], c2: [f32; 4]) -> f32 {
2788 let l1 = relative_luminance(c1);
2789 let l2 = relative_luminance(c2);
2790 let (light, dark) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
2791 (light + 0.05) / (dark + 0.05)
2792 }
2793}
2794#[derive(
2796 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
2797)]
2798pub enum RenderTier {
2799 Tier1GPU = 0,
2801 Tier2GPU = 1,
2803 Tier3Fallback = 2,
2805}
2806use bytemuck::{Pod, Zeroable};
2810#[repr(C)]
2812#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2813pub struct ColorTheme {
2814 pub primary_neon: [f32; 4], pub shatter_neon: [f32; 4],
2816 pub glass_base: [f32; 4],
2817 pub glass_edge: [f32; 4],
2818 pub rune_glow: [f32; 4],
2819 pub ember_core: [f32; 4],
2820 pub background_deep: [f32; 4],
2821 pub mani_glow: [f32; 4], pub glass_blur_strength: f32,
2823 pub shatter_edge_width: f32,
2824 pub neon_bloom_radius: f32,
2825 pub rune_opacity: f32,
2826 pub glass_tint_adapt: f32,
2829 pub glass_ior: f32,
2831 pub color_space: u32,
2833 pub _pad0: f32,
2835 pub _pad1: f32,
2836 pub _pad2: f32,
2837 pub _pad3: f32,
2838 pub _pad4: f32,
2839}
2840const _: () = assert!(
2844 std::mem::size_of::<ColorTheme>() == 176,
2845 "ColorTheme Rust/WGSL layout mismatch: expected 176 bytes"
2846);
2847impl ColorTheme {
2848 pub fn asgard() -> Self {
2850 Self {
2851 primary_neon: [0.0, 1.0, 0.95, 1.2],
2852 shatter_neon: [1.0, 0.0, 0.75, 1.5],
2853 glass_base: [0.04, 0.04, 0.06, 0.82],
2854 glass_edge: [0.0, 0.45, 0.55, 0.6],
2855 rune_glow: [0.75, 0.98, 1.0, 0.9],
2856 ember_core: [0.95, 0.12, 0.12, 1.0],
2857 background_deep: [0.01, 0.01, 0.03, 1.0],
2858 mani_glow: [0.7, 0.9, 1.0, 0.05],
2859 glass_blur_strength: 0.6,
2860 shatter_edge_width: 1.8,
2861 neon_bloom_radius: 0.022,
2862 rune_opacity: 0.55,
2863 glass_tint_adapt: 0.35,
2864 glass_ior: 1.45,
2865 color_space: 0,
2866 _pad0: 0.0,
2867 _pad1: 0.0,
2868 _pad2: 0.0,
2869 _pad3: 0.0,
2870 _pad4: 0.0,
2871 }
2872 }
2873
2874 pub fn midgard() -> Self {
2876 Self {
2877 primary_neon: [0.2, 0.4, 0.6, 1.0], shatter_neon: [0.5, 0.5, 0.5, 1.0], glass_base: [0.1, 0.12, 0.15, 1.0], glass_edge: [0.3, 0.35, 0.4, 1.0], rune_glow: [0.8, 0.8, 0.8, 0.0], ember_core: [0.5, 0.5, 0.5, 1.0],
2883 background_deep: [0.05, 0.05, 0.07, 1.0],
2884 mani_glow: [0.0, 0.0, 0.0, 0.0], glass_blur_strength: 0.0, shatter_edge_width: 1.0,
2887 neon_bloom_radius: 0.0,
2888 rune_opacity: 0.0,
2889 glass_tint_adapt: 0.0,
2890 glass_ior: 1.0,
2891 color_space: 0,
2892 _pad0: 0.0,
2893 _pad1: 0.0,
2894 _pad2: 0.0,
2895 _pad3: 0.0,
2896 _pad4: 0.0,
2897 }
2898 }
2899
2900 pub fn cyberpunk_viking() -> Self {
2901 Self::asgard()
2902 }
2903 pub fn vibrant_glass() -> Self {
2904 Self {
2905 primary_neon: [0.0, 1.0, 0.95, 1.2],
2906 shatter_neon: [1.0, 0.0, 0.75, 1.5],
2907 glass_base: [0.55, 0.6, 0.7, 0.08], glass_edge: [0.7, 0.85, 1.0, 0.45], rune_glow: [0.75, 0.98, 1.0, 0.9],
2910 ember_core: [1.0, 0.4, 0.1, 1.0],
2911 background_deep: [0.05, 0.05, 0.1, 1.0],
2912 mani_glow: [0.7, 0.9, 1.0, 0.05],
2913 glass_blur_strength: 0.9,
2914 shatter_edge_width: 1.8,
2915 neon_bloom_radius: 0.022,
2916 rune_opacity: 0.55,
2917 glass_tint_adapt: 0.65,
2918 glass_ior: 1.45,
2919 color_space: 0,
2920 _pad0: 0.0,
2921 _pad1: 0.0,
2922 _pad2: 0.0,
2923 _pad3: 0.0,
2924 _pad4: 0.0,
2925 }
2926 }
2927
2928 pub fn berserker() -> Self {
2930 Self {
2931 primary_neon: [1.0, 0.08, 0.12, 1.8],
2932 shatter_neon: [0.95, 0.92, 0.88, 1.6],
2933 glass_base: [0.03, 0.02, 0.02, 0.88],
2934 glass_edge: [0.8, 0.35, 0.08, 0.7],
2935 rune_glow: [0.9, 0.72, 0.3, 1.0],
2936 ember_core: [0.98, 0.25, 0.05, 1.0],
2937 background_deep: [0.01, 0.005, 0.005, 1.0],
2938 mani_glow: [0.8, 0.2, 0.05, 0.08],
2939 glass_blur_strength: 0.85,
2940 shatter_edge_width: 2.8,
2941 neon_bloom_radius: 0.035,
2942 rune_opacity: 0.85,
2943 glass_tint_adapt: 0.15,
2944 glass_ior: 1.85,
2945 color_space: 0,
2946 _pad0: 0.0,
2947 _pad1: 0.0,
2948 _pad2: 0.0,
2949 _pad3: 0.0,
2950 _pad4: 0.0,
2951 }
2952 }
2953}
2954impl Default for ColorTheme {
2955 fn default() -> Self {
2956 Self::vibrant_glass()
2957 }
2958}
2959#[repr(C)]
2961#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2962pub struct SceneUniforms {
2963 pub view: glam::Mat4,
2964 pub proj: glam::Mat4,
2965 pub time: f32,
2966 pub delta_time: f32,
2967 pub resolution: [f32; 2],
2968 pub mouse: [f32; 2],
2969 pub mouse_velocity: [f32; 2],
2970 pub shatter_origin: [f32; 2],
2971 pub shatter_time: f32,
2972 pub shatter_force: f32,
2973 pub berzerker_rage: f32,
2974 pub berzerker_mode: u32,
2975 pub scroll_offset: f32,
2976 pub scale_factor: f32,
2977 pub scene_type: u32,
2978 pub _pad_vec2_align: [u32; 1], pub fireball_pos: [f32; 2],
2980 pub _pad: [f32; 4], }
2982
2983pub const SCENE_AURORA: u32 = 0;
2984pub const SCENE_VOID: u32 = 1;
2985pub const SCENE_NEBULA: u32 = 2;
2986pub const SCENE_GLITCH: u32 = 3;
2987pub const SCENE_YGGDRASIL: u32 = 4;
2988
2989pub fn resolve_scene_by_name(name: &str) -> Option<u32> {
2994 let normalized = name.to_lowercase().replace(['-', '_', ' ', '.'], "");
2995 match normalized.as_str() {
2996 "aurora" => Some(SCENE_AURORA),
2997 "void" | "empty" | "none" | "blank" => Some(SCENE_VOID),
2998 "nebula" => Some(SCENE_NEBULA),
2999 "glitch" => Some(SCENE_GLITCH),
3000 "yggdrasil" | "worldtree" | "tree" => Some(SCENE_YGGDRASIL),
3001 _ => None,
3002 }
3003}
3004
3005impl SceneUniforms {
3006 pub fn new(width: f32, height: f32) -> Self {
3007 Self {
3008 view: glam::Mat4::IDENTITY,
3009 proj: glam::Mat4::orthographic_lh(0.0, width, height, 0.0, -100.0, 100.0),
3010 time: 0.0,
3011 delta_time: 0.016,
3012 resolution: [width, height],
3013 mouse: [0.5, 0.5],
3014 mouse_velocity: [0.0, 0.0],
3015 shatter_origin: [0.5, 0.5],
3016 shatter_time: -100.0,
3017 shatter_force: 0.0,
3018 berzerker_rage: 0.0,
3019 berzerker_mode: 0,
3020 scroll_offset: 0.0,
3021 scale_factor: 1.0,
3022 scene_type: SCENE_AURORA,
3023 _pad_vec2_align: [0],
3024 fireball_pos: [0.0, 0.0],
3025 _pad: [0.0; 4],
3026 }
3027 }
3028}
3029#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3031pub struct Mesh {
3032 pub vertices: Vec<[f32; 3]>,
3033 pub normals: Vec<[f32; 3]>,
3034 pub indices: Vec<u32>,
3035}
3036impl Mesh {
3037 pub fn from_obj(data: &[u8]) -> anyhow::Result<Vec<Self>> {
3038 let mut cursor = std::io::Cursor::new(data);
3039 let (models, _) = tobj::load_obj_buf(&mut cursor, &tobj::LoadOptions::default(), |_| {
3040 Ok((Vec::new(), Default::default()))
3041 })?;
3042 let mut meshes = Vec::new();
3043 for m in models {
3044 let mesh = m.mesh;
3045 let vertices: Vec<[f32; 3]> = mesh
3046 .positions
3047 .chunks_exact(3)
3048 .map(|c| [c[0], c[1], c[2]])
3049 .collect();
3050 let normals = if mesh.normals.is_empty() {
3051 vec![[0.0, 0.0, 1.0]; vertices.len()]
3052 } else {
3053 mesh.normals.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
3054 };
3055 meshes.push(Mesh {
3056 vertices,
3057 normals,
3058 indices: mesh.indices,
3059 });
3060 }
3061 Ok(meshes)
3062 }
3063 pub fn from_stl(data: &[u8]) -> anyhow::Result<Self> {
3064 let mut cursor = std::io::Cursor::new(data);
3065 let stl = stl_io::read_stl(&mut cursor)?;
3066 let vertices: Vec<[f32; 3]> = stl.vertices.iter().map(|v| [v[0], v[1], v[2]]).collect();
3067 let mut indices = Vec::new();
3068 for face in stl.faces {
3069 indices.push(face.vertices[0] as u32);
3070 indices.push(face.vertices[1] as u32);
3071 indices.push(face.vertices[2] as u32);
3072 }
3073 let normals = vec![[0.0, 0.0, 1.0]; vertices.len()];
3074 Ok(Mesh {
3075 vertices,
3076 normals,
3077 indices,
3078 })
3079 }
3080}
3081
3082#[derive(Debug, Clone, Copy, PartialEq)]
3088pub struct Transform3D {
3089 pub position: glam::Vec3,
3090 pub rotation: glam::Quat,
3091 pub scale: glam::Vec3,
3092}
3093
3094impl Default for Transform3D {
3095 fn default() -> Self {
3096 Self {
3097 position: glam::Vec3::ZERO,
3098 rotation: glam::Quat::IDENTITY,
3099 scale: glam::Vec3::ONE,
3100 }
3101 }
3102}
3103
3104impl Transform3D {
3105 pub fn to_matrix(&self) -> glam::Mat4 {
3107 glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
3108 }
3109
3110 pub fn from_2d(x: f32, y: f32, rotation: f32) -> Self {
3112 Self {
3113 position: glam::Vec3::new(x, y, 0.0),
3114 rotation: glam::Quat::from_rotation_z(rotation),
3115 scale: glam::Vec3::ONE,
3116 }
3117 }
3118}
3119
3120#[derive(Debug, Clone, Copy)]
3122pub struct Camera3D {
3123 pub position: glam::Vec3,
3125 pub target: glam::Vec3,
3127 pub up: glam::Vec3,
3129 pub fov_y: f32,
3131 pub near: f32,
3133 pub far: f32,
3135 pub perspective: bool,
3137 pub aspect: f32,
3139}
3140
3141#[derive(Debug, Clone, Copy, PartialEq)]
3143pub struct Material3D {
3144 pub base_color: [f32; 4],
3146 pub metallic: f32,
3148 pub roughness: f32,
3150 pub emissive: [f32; 3],
3152 pub opacity: f32,
3154}
3155
3156impl Default for Material3D {
3157 fn default() -> Self {
3158 Self {
3159 base_color: [1.0, 1.0, 1.0, 1.0],
3160 metallic: 0.0,
3161 roughness: 0.5,
3162 emissive: [0.0, 0.0, 0.0],
3163 opacity: 1.0,
3164 }
3165 }
3166}
3167
3168impl Material3D {
3169 pub fn unlit(color: [f32; 4]) -> Self {
3171 Self {
3172 base_color: color,
3173 metallic: 0.0,
3174 roughness: 1.0,
3175 emissive: [0.0, 0.0, 0.0],
3176 opacity: color[3],
3177 }
3178 }
3179
3180 pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
3182 Self {
3183 base_color: color,
3184 metallic: 1.0,
3185 roughness: roughness.clamp(0.0, 1.0),
3186 emissive: [0.0, 0.0, 0.0],
3187 opacity: color[3],
3188 }
3189 }
3190}
3191
3192impl Default for Camera3D {
3193 fn default() -> Self {
3194 Self {
3195 position: glam::Vec3::new(0.0, 0.0, 10.0),
3196 target: glam::Vec3::ZERO,
3197 up: glam::Vec3::Y,
3198 fov_y: 45.0f32.to_radians(),
3199 near: 0.1,
3200 far: 1000.0,
3201 perspective: true,
3202 aspect: 16.0 / 9.0,
3203 }
3204 }
3205}
3206
3207impl Camera3D {
3208 pub fn view_matrix(&self) -> glam::Mat4 {
3210 glam::Mat4::look_at_lh(self.position, self.target, self.up)
3211 }
3212
3213 pub fn projection_matrix(&self) -> glam::Mat4 {
3215 if self.perspective {
3216 glam::Mat4::perspective_lh(self.fov_y, self.aspect, self.near, self.far)
3217 } else {
3218 let top = self.fov_y;
3220 let right = top * self.aspect;
3221 glam::Mat4::orthographic_lh(-right, right, -top, top, self.near, self.far)
3222 }
3223 }
3224
3225 pub fn view_projection(&self) -> glam::Mat4 {
3227 self.projection_matrix() * self.view_matrix()
3228 }
3229}
3230
3231pub trait FrameRenderer<E = ()>: Renderer {
3234 fn begin_frame(&mut self) -> E;
3235 fn render_frame(&mut self) {
3236 }
3238 fn end_frame(&mut self, encoder: E);
3239}
3240use std::sync::Arc;
3241type SubscriberList<T> = Arc<std::sync::Mutex<Vec<Box<dyn Fn(&T) + Send + Sync>>>>;
3242
3243fn invoke_subscribers_safely<T>(subs: &SubscriberList<T>, val: &T) -> usize
3249where
3250 {
3254 let guard = match subs.lock() {
3260 Ok(g) => g,
3261 Err(poisoned) => {
3262 log::warn!(
3263 "[State] subscriber list mutex was poisoned; recovering"
3264 );
3265 poisoned.into_inner()
3266 }
3267 };
3268 let mut invoked = 0usize;
3269 for cb in guard.iter() {
3270 let cb_ref: &(dyn Fn(&T) + Send + Sync) = &**cb;
3274 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3278 cb_ref(val);
3279 }));
3280 if let Err(payload) = result {
3281 let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
3282 (*s).to_string()
3283 } else if let Some(s) = payload.downcast_ref::<String>() {
3284 s.clone()
3285 } else {
3286 "unknown panic payload".to_string()
3287 };
3288 log::error!("[State] subscriber callback panicked: {msg}");
3289 } else {
3291 invoked += 1;
3292 }
3293 }
3294 invoked
3295}
3296#[derive(Clone)]
3316pub struct State<T: Clone + Send + Sync + 'static> {
3317 swap: Arc<arc_swap::ArcSwap<T>>,
3318 metadata_swap: Arc<arc_swap::ArcSwap<Option<agents::MutationMetadata>>>,
3319 #[cfg(not(target_arch = "wasm32"))]
3320 tvar: Arc<stm::TVar<T>>,
3321 #[cfg(not(target_arch = "wasm32"))]
3322 metadata_tvar: Arc<stm::TVar<Option<agents::MutationMetadata>>>,
3323 subscribers: SubscriberList<T>,
3324 version: Arc<std::sync::atomic::AtomicU64>,
3325 resolution: agents::ConflictResolution,
3326}
3327impl<T: Clone + Send + Sync + 'static> State<T> {
3328 pub fn new(value: T) -> Self {
3330 #[cfg(not(target_arch = "wasm32"))]
3331 let tvar = Arc::new(stm::TVar::new(value.clone()));
3332 #[cfg(not(target_arch = "wasm32"))]
3333 let metadata_tvar = Arc::new(stm::TVar::new(None));
3334 Self {
3335 swap: Arc::new(arc_swap::ArcSwap::from_pointee(value)),
3336 metadata_swap: Arc::new(arc_swap::ArcSwap::new(Arc::new(None))),
3337 #[cfg(not(target_arch = "wasm32"))]
3338 tvar,
3339 #[cfg(not(target_arch = "wasm32"))]
3340 metadata_tvar,
3341 subscribers: Arc::new(std::sync::Mutex::new(Vec::new())),
3342 version: Arc::new(std::sync::atomic::AtomicU64::new(0)),
3343 resolution: agents::ConflictResolution::default(),
3344 }
3345 }
3346 pub fn with_resolution(mut self, resolution: agents::ConflictResolution) -> Self {
3348 self.resolution = resolution;
3349 self
3350 }
3351 pub fn get(&self) -> T {
3353 (**self.swap.load()).clone()
3354 }
3355 pub fn set(&self, value: T) {
3357 #[cfg(not(target_arch = "wasm32"))]
3358 let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3359 let new_meta = agents::get_current_mutation_metadata();
3360 let existing_meta = self.metadata_tvar.read(tx)?;
3361 let mut skip = false;
3362 if self.resolution == agents::ConflictResolution::PriorityWins
3363 && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3364 && new_m.priority < old_m.priority
3365 {
3366 skip = true;
3367 }
3368 if !skip {
3369 self.tvar.write(tx, value.clone())?;
3370 self.metadata_tvar.write(tx, new_meta)?;
3371 Ok((false, value.clone(), new_meta))
3372 } else {
3373 Ok((true, self.tvar.read(tx)?, existing_meta))
3374 }
3375 });
3376 #[cfg(target_arch = "wasm32")]
3377 let (was_skipped, final_val, final_meta) =
3378 (false, value, agents::get_current_mutation_metadata());
3379 if was_skipped {
3380 if let (Some(new_m), Some(old_m)) =
3381 (agents::get_current_mutation_metadata(), final_meta)
3382 {
3383 agents::notify_conflict(agents::ConflictEvent {
3384 agent_id: new_m.agent_id,
3385 priority: new_m.priority,
3386 existing_agent_id: old_m.agent_id,
3387 existing_priority: old_m.priority,
3388 timestamp_ms: new_m.timestamp_ms,
3389 });
3390 }
3391 return;
3392 }
3393 self.swap.store(Arc::new(final_val.clone()));
3394 self.metadata_swap.store(Arc::new(final_meta));
3395 self.version
3396 .fetch_add(1, std::sync::atomic::Ordering::Release);
3397 let subs = Arc::clone(&self.subscribers);
3398 if crate::is_batching() {
3399 crate::enqueue_batch_task(Box::new(move || {
3400 let _ = invoke_subscribers_safely(&subs, &final_val);
3401 }));
3402 } else {
3403 let _ = invoke_subscribers_safely(&subs, &final_val);
3404 }
3405 }
3406
3407 pub fn set_direct(&self, value: T) {
3421 self.swap.store(Arc::new(value.clone()));
3422 let new_meta = agents::get_current_mutation_metadata();
3423 self.metadata_swap.store(Arc::new(new_meta));
3424 self.version
3425 .fetch_add(1, std::sync::atomic::Ordering::Release);
3426 #[cfg(not(target_arch = "wasm32"))]
3427 {
3428 let _ = stm::atomically(|tx| {
3429 self.tvar.write(tx, value.clone())?;
3430 let meta = agents::get_current_mutation_metadata();
3431 self.metadata_tvar.write(tx, meta)?;
3432 Ok(())
3433 });
3434 }
3435 let subs = Arc::clone(&self.subscribers);
3436 if crate::is_batching() {
3437 crate::enqueue_batch_task(Box::new(move || {
3438 let _ = invoke_subscribers_safely(&subs, &value);
3439 }));
3440 } else {
3441 let _ = invoke_subscribers_safely(&subs, &value);
3442 }
3443 }
3444 pub fn mutate<F: Fn(&T) -> T>(&self, f: F) {
3445 #[cfg(not(target_arch = "wasm32"))]
3446 {
3447 let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3448 let new_meta = agents::get_current_mutation_metadata();
3449 let existing_meta = self.metadata_tvar.read(tx)?;
3450 let mut skip = false;
3451 if self.resolution == agents::ConflictResolution::PriorityWins
3452 && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3453 && new_m.priority < old_m.priority
3454 {
3455 skip = true;
3456 }
3457 if !skip {
3458 let current = self.tvar.read(tx)?;
3459 let next = f(¤t);
3460 self.tvar.write(tx, next.clone())?;
3461 self.metadata_tvar.write(tx, new_meta)?;
3462 Ok((false, next, new_meta))
3463 } else {
3464 Ok((true, self.tvar.read(tx)?, existing_meta))
3465 }
3466 });
3467 if was_skipped {
3468 if let (Some(new_m), Some(old_m)) =
3469 (agents::get_current_mutation_metadata(), final_meta)
3470 {
3471 agents::notify_conflict(agents::ConflictEvent {
3472 agent_id: new_m.agent_id,
3473 priority: new_m.priority,
3474 existing_agent_id: old_m.agent_id,
3475 existing_priority: old_m.priority,
3476 timestamp_ms: new_m.timestamp_ms,
3477 });
3478 }
3479 return;
3480 }
3481 self.swap.store(Arc::new(final_val.clone()));
3482 self.metadata_swap.store(Arc::new(final_meta));
3483 self.version
3484 .fetch_add(1, std::sync::atomic::Ordering::Release);
3485 let subs = Arc::clone(&self.subscribers);
3486 if crate::is_batching() {
3487 crate::enqueue_batch_task(Box::new(move || {
3488 let _ = invoke_subscribers_safely(&subs, &final_val);
3489 }));
3490 } else {
3491 let _ = invoke_subscribers_safely(&subs, &final_val);
3492 }
3493 }
3494 #[cfg(target_arch = "wasm32")]
3495 {
3496 self.set(f(&self.get()));
3497 }
3498 }
3499 pub fn version(&self) -> u64 {
3501 self.version.load(std::sync::atomic::Ordering::Acquire)
3502 }
3503 pub fn subscribe<F: Fn(&T) + Send + Sync + 'static>(&self, callback: F) {
3505 self.subscribers.lock().unwrap_or_else(|p| p.into_inner()).push(Box::new(callback));
3506 }
3507}
3508use crate::runtime::NodeStateSnapshot;
3509use std::sync::OnceLock;
3510use std::sync::atomic::{AtomicBool, Ordering};
3511
3512#[cfg(not(target_arch = "wasm32"))]
3528static FALLBACK_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
3529
3530#[cfg(not(target_arch = "wasm32"))]
3533static FALLBACK_WORKER_COUNT: OnceLock<usize> = OnceLock::new();
3534
3535#[cfg(not(target_arch = "wasm32"))]
3536fn fallback_runtime() -> &'static tokio::runtime::Runtime {
3537 FALLBACK_RUNTIME.get_or_init(|| {
3538 let worker_count = *FALLBACK_WORKER_COUNT.get_or_init(|| {
3542 let available = std::thread::available_parallelism()
3543 .map(|n| n.get())
3544 .unwrap_or(2);
3545 available.saturating_sub(1).clamp(1, 8)
3546 });
3547 tokio::runtime::Builder::new_current_thread()
3548 .worker_threads(worker_count)
3549 .thread_name("cvkg-fallback-rt")
3550 .enable_all()
3551 .build()
3552 .expect("failed to build fallback tokio runtime")
3553 })
3554}
3555pub static SYSTEM_STATE: OnceLock<Arc<arc_swap::ArcSwap<AppState>>> = OnceLock::new();
3557#[cfg(not(target_arch = "wasm32"))]
3558static KNOWLEDGE_TVAR: OnceLock<stm::TVar<AppState>> = OnceLock::new();
3559static IS_BATCHING: AtomicBool = AtomicBool::new(false);
3560pub static IS_RENDERING: AtomicBool = AtomicBool::new(false);
3561pub static LAYOUT_DIRTY: AtomicBool = AtomicBool::new(false);
3562type BatchQueue = OnceLock<std::sync::Mutex<Vec<Box<dyn FnOnce() + Send + Sync>>>>;
3563static BATCH_QUEUE: BatchQueue = OnceLock::new();
3564static STATE_WRITE_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3567pub fn is_batching() -> bool {
3569 IS_BATCHING.load(Ordering::Acquire)
3570}
3571pub fn is_rendering() -> bool {
3573 IS_RENDERING.load(Ordering::Acquire)
3574}
3575pub fn begin_render_phase() {
3577 IS_RENDERING.store(true, Ordering::Release);
3578}
3579pub fn end_render_phase() {
3581 IS_RENDERING.store(false, Ordering::Release);
3582}
3583pub fn enqueue_batch_task(task: Box<dyn FnOnce() + Send + Sync>) {
3585 let mut queue = BATCH_QUEUE
3586 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3587 .lock()
3588 .unwrap_or_else(|p| p.into_inner());
3589 queue.push(task);
3590}
3591pub fn batch<F: FnOnce()>(f: F) {
3595 if IS_BATCHING.swap(true, Ordering::AcqRel) {
3596 f();
3598 return;
3599 }
3600 f();
3601 IS_BATCHING.store(false, Ordering::Release);
3602 let mut queue = BATCH_QUEUE
3603 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3604 .lock()
3605 .unwrap();
3606 let tasks: Vec<_> = queue.drain(..).collect();
3607 drop(queue);
3608 for task in tasks {
3609 task();
3610 }
3611}
3612pub fn get_system_state() -> Arc<arc_swap::ArcSwap<AppState>> {
3614 SYSTEM_STATE
3615 .get_or_init(|| Arc::new(arc_swap::ArcSwap::from_pointee(AppState::default())))
3616 .clone()
3617}
3618pub fn load_system_state() -> arc_swap::Guard<Arc<AppState>> {
3619 get_system_state().load()
3620}
3621pub fn update_system_state<F>(f: F)
3622where
3623 F: FnOnce(&AppState) -> AppState,
3624{
3625 let _lock = STATE_WRITE_MUTEX.lock().unwrap_or_else(|p| p.into_inner());
3626 if is_rendering() {
3627 log::warn!(
3628 "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3629 );
3630 }
3631 LAYOUT_DIRTY.store(true, Ordering::SeqCst);
3632 let swap = get_system_state();
3633 let current = swap.load();
3634 let new_state = Arc::new(f(¤t));
3635 swap.store(Arc::clone(&new_state));
3636 #[cfg(not(target_arch = "wasm32"))]
3637 {
3638 let tvar = KNOWLEDGE_TVAR.get_or_init(|| stm::TVar::new((*new_state).clone()));
3639 stm::atomically(|tx| tvar.write(tx, (*new_state).clone()));
3640 }
3641}
3642pub fn transact_system_state<F>(f: F)
3643where
3644 F: Fn(&AppState) -> AppState,
3645{
3646 let _lock = STATE_WRITE_MUTEX.lock().unwrap_or_else(|p| p.into_inner());
3647 #[cfg(not(target_arch = "wasm32"))]
3648 {
3649 if is_rendering() {
3650 log::warn!(
3651 "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3652 );
3653 }
3654 let tvar = KNOWLEDGE_TVAR
3655 .get_or_init(|| stm::TVar::new((**get_system_state().load()).clone()))
3656 .clone();
3657 let new_state = stm::atomically(move |tx| {
3658 let current = tvar.read(tx)?;
3659 let next = f(¤t);
3660 tvar.write(tx, next.clone())?;
3661 Ok(next)
3662 });
3663 get_system_state().store(Arc::new(new_state));
3664 }
3665 #[cfg(target_arch = "wasm32")]
3666 {
3667 if is_rendering() {
3668 log::warn!(
3669 "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3670 );
3671 }
3672 update_system_state(f);
3673 }
3674}
3675impl AppState {
3676 pub fn new() -> Self {
3678 Self::default()
3679 }
3680 pub fn set_component_state<T: 'static + Send + Sync>(&mut self, id: u64, state: T) {
3682 self.component_states
3683 .insert(id, Arc::new(std::sync::RwLock::new(state)));
3684 }
3685 pub fn get_component_state<T: 'static + Send + Sync>(
3687 &self,
3688 id: u64,
3689 ) -> Option<Arc<std::sync::RwLock<T>>> {
3690 let stored = self.component_states.get(&id)?;
3691 let any_ref = stored.read().ok()?;
3695 let _verified: &T = any_ref.downcast_ref::<T>()?;
3697 drop(any_ref);
3698 let raw = Arc::into_raw(stored.clone());
3701 Some(unsafe { Arc::from_raw(raw as *const std::sync::RwLock<T>) })
3702 }
3703 pub fn remember(&mut self, fragment: KnowledgeFragment) {
3705 self.fragments.insert(fragment.id.clone(), fragment);
3706 }
3707 pub fn process_query(&mut self, query: &str) {
3709 let query_lower = query.to_lowercase();
3710 let mut results: Vec<(f32, String)> = self
3711 .fragments
3712 .iter()
3713 .map(|(id, frag)| {
3714 let mut score = 0.0;
3715 if frag.summary.to_lowercase().contains(&query_lower) {
3716 score += 1.0;
3717 }
3718 if frag.source.to_lowercase().contains(&query_lower) {
3719 score += 0.5;
3720 }
3721 (score, id.clone())
3722 })
3723 .filter(|(score, _)| *score > 0.0)
3724 .collect();
3725 results.sort_by(|a, b| b.0.total_cmp(&a.0));
3727 self.last_query_results = results.into_iter().map(|(_, id)| id).take(5).collect();
3728 }
3729 pub fn snapshot(&self) -> Vec<NodeStateSnapshot> {
3731 let mut snapshots = Vec::new();
3732 for frag in self.fragments.values() {
3734 if let Ok(val) = serde_json::to_value(frag) {
3735 snapshots.push(NodeStateSnapshot { id: 0, state: val });
3736 }
3737 }
3738 snapshots
3739 }
3740}
3741#[derive(Clone)]
3743pub struct Binding<T: Clone + Send + Sync + 'static> {
3744 swap: Arc<arc_swap::ArcSwap<T>>,
3745 #[cfg(not(target_arch = "wasm32"))]
3746 tvar: Arc<stm::TVar<T>>,
3747 version: Arc<std::sync::atomic::AtomicU64>,
3748}
3749impl<T: Clone + Send + Sync + 'static> Binding<T> {
3750 pub fn from_state(state: &State<T>) -> Self {
3752 Self {
3753 swap: Arc::clone(&state.swap),
3754 #[cfg(not(target_arch = "wasm32"))]
3755 tvar: Arc::clone(&state.tvar),
3756 version: Arc::clone(&state.version),
3757 }
3758 }
3759 pub fn get(&self) -> T {
3761 (**self.swap.load()).clone()
3762 }
3763 pub fn set(&self, value: T) {
3765 self.swap.store(Arc::new(value.clone()));
3766 #[cfg(not(target_arch = "wasm32"))]
3767 {
3768 let tvar = Arc::clone(&self.tvar);
3769 let v = value.clone();
3770 stm::atomically(move |tx| tvar.write(tx, v.clone()));
3771 }
3772 self.version
3773 .fetch_add(1, std::sync::atomic::Ordering::Release);
3774 }
3775 pub fn version(&self) -> u64 {
3777 self.version.load(std::sync::atomic::Ordering::Acquire)
3778 }
3779}
3780#[cfg(not(target_arch = "wasm32"))]
3781pub fn transact_pair<A, B, F>(state_a: &State<A>, state_b: &State<B>, f: F)
3782where
3783 A: Clone + Send + Sync + 'static,
3784 B: Clone + Send + Sync + 'static,
3785 F: Fn(&A, &B) -> (A, B),
3786{
3787 let tvar_a = Arc::clone(&state_a.tvar);
3788 let tvar_b = Arc::clone(&state_b.tvar);
3789 let (new_a, new_b) = stm::atomically(move |tx| {
3790 let a = tvar_a.read(tx)?;
3791 let b = tvar_b.read(tx)?;
3792 let (na, nb) = f(&a, &b);
3793 tvar_a.write(tx, na.clone())?;
3794 tvar_b.write(tx, nb.clone())?;
3795 Ok((na, nb))
3796 });
3797 state_a.swap.store(Arc::new(new_a.clone()));
3798 state_b.swap.store(Arc::new(new_b.clone()));
3799 state_a
3800 .version
3801 .fetch_add(1, std::sync::atomic::Ordering::Release);
3802 state_b
3803 .version
3804 .fetch_add(1, std::sync::atomic::Ordering::Release);
3805 let subs_a = Arc::clone(&state_a.subscribers);
3806 let subs_b = Arc::clone(&state_b.subscribers);
3807 if crate::is_batching() {
3808 crate::enqueue_batch_task(Box::new(move || {
3809 {
3810 let s = subs_a.lock().unwrap_or_else(|p| p.into_inner());
3811 for cb in s.iter() {
3812 cb(&new_a);
3813 }
3814 }
3815 {
3816 let s = subs_b.lock().unwrap_or_else(|p| p.into_inner());
3817 for cb in s.iter() {
3818 cb(&new_b);
3819 }
3820 }
3821 }));
3822 } else {
3823 {
3824 let s = subs_a.lock().unwrap_or_else(|p| p.into_inner());
3825 for cb in s.iter() {
3826 cb(&new_a);
3827 }
3828 }
3829 {
3830 let s = subs_b.lock().unwrap_or_else(|p| p.into_inner());
3831 for cb in s.iter() {
3832 cb(&new_b);
3833 }
3834 }
3835 }
3836}
3837use std::any::TypeId;
3838use std::sync::Mutex;
3839pub(crate) static ENVIRONMENT: OnceLock<
3841 Mutex<HashMap<TypeId, Box<dyn std::any::Any + Send + Sync>>>,
3842> = OnceLock::new();
3843pub trait EnvKey: 'static + Send + Sync {
3846 type Value: Clone + Send + Sync + 'static;
3848 fn default_value() -> Self::Value;
3850}
3851pub struct YggdrasilKey;
3853impl EnvKey for YggdrasilKey {
3854 type Value = DesignTokens;
3855 fn default_value() -> Self::Value {
3856 default_tokens()
3857 }
3858}
3859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3862pub enum Appearance {
3863 Light,
3864 Dark,
3865}
3866#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3868pub enum Orientation {
3869 Horizontal,
3870 Vertical,
3871}
3872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3874pub struct GridPlacement {
3875 pub column: i32,
3877 pub column_span: u32,
3879 pub row: i32,
3881 pub row_span: u32,
3883}
3884#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3886pub enum Alignment {
3887 #[default]
3888 Center,
3889 Leading,
3890 Trailing,
3891 Top,
3892 Bottom,
3893}
3894#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3896pub enum Distribution {
3897 #[default]
3898 Fill,
3899 Center,
3900 Leading,
3901 Trailing,
3902 SpaceBetween,
3903 SpaceAround,
3904 SpaceEvenly,
3905}
3906#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3908pub struct Color {
3909 pub r: f32,
3910 pub g: f32,
3911 pub b: f32,
3912 pub a: f32,
3913}
3914impl Color {
3915 pub const BLACK: Color = Color {
3916 r: 0.0,
3917 g: 0.0,
3918 b: 0.0,
3919 a: 1.0,
3920 };
3921 pub const WHITE: Color = Color {
3922 r: 1.0,
3923 g: 1.0,
3924 b: 1.0,
3925 a: 1.0,
3926 };
3927 pub const TRANSPARENT: Color = Color {
3928 r: 0.0,
3929 g: 0.0,
3930 b: 0.0,
3931 a: 0.0,
3932 };
3933 pub const RED: Color = Color {
3934 r: 1.0,
3935 g: 0.0,
3936 b: 0.0,
3937 a: 1.0,
3938 };
3939 pub const GREEN: Color = Color {
3940 r: 0.0,
3941 g: 1.0,
3942 b: 0.0,
3943 a: 1.0,
3944 };
3945 pub const BLUE: Color = Color {
3946 r: 0.0,
3947 g: 0.0,
3948 b: 1.0,
3949 a: 1.0,
3950 };
3951 pub const VIKING_GOLD: Color = Color {
3952 r: 1.0,
3953 g: 0.84,
3954 b: 0.0,
3955 a: 1.0,
3956 };
3957 pub const MAGENTA_LIQUID: Color = Color {
3958 r: 1.0,
3959 g: 0.0,
3960 b: 1.0,
3961 a: 1.0,
3962 };
3963 pub const TACTICAL_OBSIDIAN: Color = Color {
3964 r: 0.05,
3965 g: 0.05,
3966 b: 0.07,
3967 a: 1.0,
3968 };
3969 pub fn relative_luminance(&self) -> f32 {
3971 fn res(c: f32) -> f32 {
3972 if c <= 0.03928 {
3973 c / 12.92
3974 } else {
3975 ((c + 0.055) / 1.055).powf(2.4)
3976 }
3977 }
3978 0.2126 * res(self.r) + 0.7152 * res(self.g) + 0.0722 * res(self.b)
3979 }
3980 pub fn contrast_ratio(&self, other: &Color) -> f32 {
3982 let l1 = self.relative_luminance();
3983 let l2 = other.relative_luminance();
3984 if l1 > l2 {
3985 (l1 + 0.05) / (l2 + 0.05)
3986 } else {
3987 (l2 + 0.05) / (l1 + 0.05)
3988 }
3989 }
3990 pub const CYAN: Color = Color {
3991 r: 0.0,
3992 g: 1.0,
3993 b: 1.0,
3994 a: 1.0,
3995 };
3996 pub const YELLOW: Color = Color {
3997 r: 1.0,
3998 g: 1.0,
3999 b: 0.0,
4000 a: 1.0,
4001 };
4002 pub const MAGENTA: Color = Color {
4003 r: 1.0,
4004 g: 0.0,
4005 b: 1.0,
4006 a: 1.0,
4007 };
4008 pub const GRAY: Color = Color {
4009 r: 0.5,
4010 g: 0.5,
4011 b: 0.5,
4012 a: 1.0,
4013 };
4014
4015 pub fn from_hex(hex: &str) -> Option<Self> {
4018 let hex = hex.strip_prefix('#').unwrap_or(hex);
4019 if hex.len() != 6 {
4020 return None;
4021 }
4022 let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
4023 let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
4024 let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
4025 Some(Color { r, g, b, a: 1.0 })
4026 }
4027 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
4029 Self { r, g, b, a }
4030 }
4031 pub fn as_array(&self) -> [f32; 4] {
4033 [self.r, self.g, self.b, self.a]
4034 }
4035
4036 pub fn lighten(&self, amount: f32) -> Self {
4042 Self {
4043 r: (self.r + amount).clamp(0.0, 1.0),
4044 g: (self.g + amount).clamp(0.0, 1.0),
4045 b: (self.b + amount).clamp(0.0, 1.0),
4046 a: self.a,
4047 }
4048 }
4049
4050 pub fn darken(&self, amount: f32) -> Self {
4052 Self {
4053 r: (self.r - amount).clamp(0.0, 1.0),
4054 g: (self.g - amount).clamp(0.0, 1.0),
4055 b: (self.b - amount).clamp(0.0, 1.0),
4056 a: self.a,
4057 }
4058 }
4059}
4060impl View for Color {
4061 type Body = Never;
4062 fn body(self) -> Self::Body {
4063 unreachable!()
4066 }
4067 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
4068 renderer.fill_rect(rect, self.as_array());
4069 }
4070}
4071pub struct AppearanceKey;
4073impl EnvKey for AppearanceKey {
4074 type Value = Appearance;
4075 fn default_value() -> Self::Value {
4076 Appearance::Dark }
4078}
4079
4080pub struct DirectionKey;
4082impl EnvKey for DirectionKey {
4083 type Value = Direction;
4084 fn default_value() -> Self::Value {
4085 Direction::LTR
4086 }
4087}
4088
4089pub struct StyleResolver;
4091impl StyleResolver {
4092 pub fn color(key: &str) -> String {
4094 let tokens = Environment::<YggdrasilKey>::new().get();
4095 let appearance = Environment::<AppearanceKey>::new().get();
4096 let is_dark = appearance == Appearance::Dark;
4097 tokens
4098 .get_color(key, is_dark)
4099 .unwrap_or_else(|| "#FF00FF".to_string()) }
4101 pub fn get<T: FromStr>(category: &str, key: &str) -> Option<T> {
4103 let tokens = Environment::<YggdrasilKey>::new().get();
4104 let appearance = Environment::<AppearanceKey>::new().get();
4105 let is_dark = appearance == Appearance::Dark;
4106 tokens.get(category, key, is_dark)
4107 }
4108 pub fn color_array(key: &str) -> [f32; 4] {
4112 let hex = Self::color(key);
4113 parse_hex_color(&hex)
4114 }
4115}
4116
4117fn parse_hex_color(hex: &str) -> [f32; 4] {
4119 let hex = hex.trim_start_matches('#');
4120 if hex.len() >= 6 {
4121 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255) as f32 / 255.0;
4122 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
4123 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255) as f32 / 255.0;
4124 let a = if hex.len() >= 8 {
4125 u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
4126 } else {
4127 1.0
4128 };
4129 [r, g, b, a]
4130 } else {
4131 [1.0, 0.0, 1.0, 1.0] }
4133}
4134
4135pub fn default_tokens() -> DesignTokens {
4137 let mut tokens = DesignTokens::new();
4138 tokens.color.insert(
4140 "background".to_string(),
4141 TokenValue::Adaptive {
4142 light: "#FFFFFF".to_string(), dark: "#000000".to_string(), },
4145 );
4146 tokens.color.insert(
4147 "primary".to_string(),
4148 TokenValue::Adaptive {
4149 light: "#007B8A".to_string(), dark: "#00FFFF".to_string(), },
4152 );
4153 tokens.color.insert(
4154 "secondary".to_string(),
4155 TokenValue::Adaptive {
4156 light: "#8A008A".to_string(), dark: "#FF00FF".to_string(), },
4159 );
4160 tokens.color.insert(
4161 "surface".to_string(),
4162 TokenValue::Adaptive {
4163 light: "#FFFFFF".to_string(),
4164 dark: "#121212".to_string(),
4165 },
4166 );
4167 tokens.color.insert(
4168 "text".to_string(),
4169 TokenValue::Adaptive {
4170 light: "#000000".to_string(),
4171 dark: "#FFFFFF".to_string(),
4172 },
4173 );
4174 tokens.color.insert(
4176 "surface_elevated".to_string(),
4177 TokenValue::Adaptive {
4178 light: "#FFFFFF".to_string(),
4179 dark: "#1A1A24".to_string(),
4180 },
4181 );
4182 tokens.color.insert(
4183 "surface_overlay".to_string(),
4184 TokenValue::Adaptive {
4185 light: "#FFFFFF".to_string(),
4186 dark: "#1E1E2E".to_string(),
4187 },
4188 );
4189 tokens.color.insert(
4190 "border".to_string(),
4191 TokenValue::Adaptive {
4192 light: "#D0D0D8".to_string(),
4193 dark: "#2A2A3A".to_string(),
4194 },
4195 );
4196 tokens.color.insert(
4197 "border_strong".to_string(),
4198 TokenValue::Adaptive {
4199 light: "#A0A0B0".to_string(),
4200 dark: "#3A3A50".to_string(),
4201 },
4202 );
4203 tokens.color.insert(
4204 "text_muted".to_string(),
4205 TokenValue::Adaptive {
4206 light: "#606070".to_string(),
4207 dark: "#8080A0".to_string(),
4208 },
4209 );
4210 tokens.color.insert(
4211 "text_dim".to_string(),
4212 TokenValue::Adaptive {
4213 light: "#9090A0".to_string(),
4214 dark: "#505070".to_string(),
4215 },
4216 );
4217 tokens.color.insert(
4218 "accent".to_string(),
4219 TokenValue::Adaptive {
4220 light: "#007B8A".to_string(), dark: "#00FFFF".to_string(), },
4223 );
4224 tokens.color.insert(
4225 "accent_hover".to_string(),
4226 TokenValue::Adaptive {
4227 light: "#00A0B0".to_string(), dark: "#33FFFF".to_string(), },
4230 );
4231 tokens.color.insert(
4232 "success".to_string(),
4233 TokenValue::Single {
4234 value: "#00E676".to_string(),
4235 },
4236 );
4237 tokens.color.insert(
4238 "warning".to_string(),
4239 TokenValue::Single {
4240 value: "#FFB300".to_string(),
4241 },
4242 );
4243 tokens.color.insert(
4244 "error".to_string(),
4245 TokenValue::Single {
4246 value: "#FF5252".to_string(),
4247 },
4248 );
4249 tokens.color.insert(
4250 "info".to_string(),
4251 TokenValue::Single {
4252 value: "#448AFF".to_string(),
4253 },
4254 );
4255 tokens.color.insert(
4256 "hover".to_string(),
4257 TokenValue::Adaptive {
4258 light: "#F0F0F5".to_string(),
4259 dark: "#252535".to_string(),
4260 },
4261 );
4262 tokens.color.insert(
4263 "active".to_string(),
4264 TokenValue::Adaptive {
4265 light: "#E0E0EB".to_string(),
4266 dark: "#303045".to_string(),
4267 },
4268 );
4269 tokens.color.insert(
4270 "disabled".to_string(),
4271 TokenValue::Adaptive {
4272 light: "#E8E8F0".to_string(),
4273 dark: "#1A1A28".to_string(),
4274 },
4275 );
4276 tokens.color.insert(
4277 "disabled_text".to_string(),
4278 TokenValue::Adaptive {
4279 light: "#B0B0C0".to_string(),
4280 dark: "#404060".to_string(),
4281 },
4282 );
4283 tokens.color.insert(
4284 "focus_ring".to_string(),
4285 TokenValue::Single {
4286 value: "#00FFFF".to_string(),
4287 },
4288 );
4289 tokens.color.insert(
4290 "shadow".to_string(),
4291 TokenValue::Adaptive {
4292 light: "#00000020".to_string(),
4293 dark: "#00000060".to_string(),
4294 },
4295 );
4296 tokens.color.insert(
4297 "code_bg".to_string(),
4298 TokenValue::Adaptive {
4299 light: "#F5F5FA".to_string(),
4300 dark: "#0D0D18".to_string(),
4301 },
4302 );
4303 tokens.bifrost.insert(
4305 "blur".to_string(),
4306 TokenValue::Single {
4307 value: "25.0".to_string(),
4308 },
4309 );
4310 tokens.bifrost.insert(
4311 "saturation".to_string(),
4312 TokenValue::Single {
4313 value: "1.2".to_string(),
4314 },
4315 );
4316 tokens.bifrost.insert(
4317 "opacity".to_string(),
4318 TokenValue::Single {
4319 value: "0.65".to_string(),
4320 },
4321 );
4322 tokens.gungnir.insert(
4324 "intensity".to_string(),
4325 TokenValue::Single {
4326 value: "1.0".to_string(),
4327 },
4328 );
4329 tokens.gungnir.insert(
4330 "radius".to_string(),
4331 TokenValue::Single {
4332 value: "15.0".to_string(),
4333 },
4334 );
4335 tokens.mjolnir.insert(
4337 "clip_angle".to_string(),
4338 TokenValue::Single {
4339 value: "12.0".to_string(),
4340 },
4341 );
4342 tokens.mjolnir.insert(
4343 "border_width".to_string(),
4344 TokenValue::Single {
4345 value: "2.0".to_string(),
4346 },
4347 );
4348 tokens.anim.insert(
4350 "stiffness".to_string(),
4351 TokenValue::Single {
4352 value: "170.0".to_string(),
4353 },
4354 );
4355 tokens.anim.insert(
4356 "damping".to_string(),
4357 TokenValue::Single {
4358 value: "26.0".to_string(),
4359 },
4360 );
4361 tokens.anim.insert(
4362 "mass".to_string(),
4363 TokenValue::Single {
4364 value: "1.0".to_string(),
4365 },
4366 );
4367 tokens.accessibility.insert(
4369 "reduce_motion".to_string(),
4370 TokenValue::Single {
4371 value: "false".to_string(),
4372 },
4373 );
4374 tokens
4375}
4376pub struct Environment<K: EnvKey> {
4378 _marker: std::marker::PhantomData<K>,
4379}
4380impl<K: EnvKey> Default for Environment<K> {
4381 fn default() -> Self {
4382 Self::new()
4383 }
4384}
4385impl<K: EnvKey> Environment<K> {
4386 pub fn new() -> Self {
4388 Self {
4389 _marker: std::marker::PhantomData,
4390 }
4391 }
4392 pub fn get(&self) -> K::Value {
4394 if let Some(env_store) = ENVIRONMENT.get() {
4395 let env_lock = env_store.lock().unwrap_or_else(|p| p.into_inner());
4396 if let Some(val) = env_lock.get(&std::any::TypeId::of::<K>()) {
4397 if let Some(typed_val) = val.downcast_ref::<K::Value>() {
4398 return typed_val.clone();
4399 } else {
4400 log::warn!(
4401 "Environment: Downcast failed for key type {:?}",
4402 std::any::type_name::<K>()
4403 );
4404 }
4405 } else {
4406 log::trace!(
4408 "Environment: Key not found: {:?}. Returning default.",
4409 std::any::type_name::<K>()
4410 );
4411 }
4412 } else {
4413 log::trace!(
4415 "Environment: Store not initialized. Key: {:?}. Returning default.",
4416 std::any::type_name::<K>()
4417 );
4418 }
4419 K::default_value()
4420 }
4421}
4422pub mod env {
4424 pub fn insert<K: super::EnvKey>(value: K::Value) {
4426 let store = super::ENVIRONMENT
4427 .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
4428 let mut env_map = store.lock().unwrap_or_else(|p| p.into_inner());
4429 env_map.insert(std::any::TypeId::of::<K>(), Box::new(value));
4430 }
4431 pub fn remove<K: super::EnvKey>() {
4433 if let Some(store) = super::ENVIRONMENT.get() {
4434 let mut env_map = store.lock().unwrap_or_else(|p| p.into_inner());
4435 env_map.remove(&std::any::TypeId::of::<K>());
4436 }
4437 }
4438}
4439#[derive(Debug, Clone, Copy, PartialEq)]
4442pub struct Size {
4443 pub width: f32,
4444 pub height: f32,
4445}
4446
4447impl Size {
4448 pub const ZERO: Self = Self {
4449 width: 0.0,
4450 height: 0.0,
4451 };
4452
4453 pub fn new(width: f32, height: f32) -> Self {
4454 Self { width, height }
4455 }
4456}
4457
4458#[derive(Debug, Clone, Copy, PartialEq)]
4460pub struct EdgeInsets {
4461 pub top: f32,
4462 pub leading: f32,
4463 pub bottom: f32,
4464 pub trailing: f32,
4465}
4466
4467impl EdgeInsets {
4468 pub fn all(value: f32) -> Self {
4470 Self {
4471 top: value,
4472 leading: value,
4473 bottom: value,
4474 trailing: value,
4475 }
4476 }
4477
4478 pub fn vertical(value: f32) -> Self {
4480 Self {
4481 top: value,
4482 leading: 0.0,
4483 bottom: value,
4484 trailing: 0.0,
4485 }
4486 }
4487
4488 pub fn horizontal(value: f32) -> Self {
4490 Self {
4491 top: 0.0,
4492 leading: value,
4493 bottom: 0.0,
4494 trailing: value,
4495 }
4496 }
4497}
4498
4499#[derive(Debug, Clone, Copy, PartialEq)]
4503pub struct FrameModifier {
4504 pub width: Option<f32>,
4506 pub height: Option<f32>,
4508 pub min_width: Option<f32>,
4510 pub max_width: Option<f32>,
4512 pub min_height: Option<f32>,
4514 pub max_height: Option<f32>,
4516 pub alignment: Alignment,
4518}
4519
4520impl Default for FrameModifier {
4521 fn default() -> Self {
4523 Self::new()
4524 }
4525}
4526
4527impl FrameModifier {
4528 pub fn new() -> Self {
4530 Self {
4531 width: None,
4532 height: None,
4533 min_width: None,
4534 max_width: None,
4535 min_height: None,
4536 max_height: None,
4537 alignment: Alignment::Center,
4538 }
4539 }
4540
4541 pub fn width(mut self, width: f32) -> Self {
4543 self.width = Some(width);
4544 self
4545 }
4546
4547 pub fn height(mut self, height: f32) -> Self {
4549 self.height = Some(height);
4550 self
4551 }
4552
4553 pub fn size(mut self, width: f32, height: f32) -> Self {
4555 self.width = Some(width);
4556 self.height = Some(height);
4557 self
4558 }
4559
4560 pub fn min_width(mut self, min_width: f32) -> Self {
4562 self.min_width = Some(min_width);
4563 self
4564 }
4565
4566 pub fn max_width(mut self, max_width: f32) -> Self {
4568 self.max_width = Some(max_width);
4569 self
4570 }
4571
4572 pub fn min_height(mut self, min_height: f32) -> Self {
4574 self.min_height = Some(min_height);
4575 self
4576 }
4577
4578 pub fn max_height(mut self, max_height: f32) -> Self {
4580 self.max_height = Some(max_height);
4581 self
4582 }
4583
4584 pub fn alignment(mut self, alignment: Alignment) -> Self {
4586 self.alignment = alignment;
4587 self
4588 }
4589}
4590
4591impl ViewModifier for FrameModifier {
4592 fn modify<V: View>(self, content: V) -> impl View {
4594 ModifiedView::new(content, self)
4595 }
4596
4597 fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
4599 let w = if let Some(width) = self.width {
4600 Some(width)
4601 } else {
4602 proposal.width.map(|pw| {
4603 pw.clamp(
4604 self.min_width.unwrap_or(0.0),
4605 self.max_width.unwrap_or(f32::INFINITY),
4606 )
4607 })
4608 };
4609 let h = if let Some(height) = self.height {
4610 Some(height)
4611 } else {
4612 proposal.height.map(|ph| {
4613 ph.clamp(
4614 self.min_height.unwrap_or(0.0),
4615 self.max_height.unwrap_or(f32::INFINITY),
4616 )
4617 })
4618 };
4619 SizeProposal {
4620 width: w,
4621 height: h,
4622 }
4623 }
4624
4625 fn transform_size(&self, child_size: Size) -> Size {
4627 let w = if let Some(width) = self.width {
4628 width
4629 } else {
4630 child_size.width.clamp(
4631 self.min_width.unwrap_or(0.0),
4632 self.max_width.unwrap_or(f32::INFINITY),
4633 )
4634 };
4635 let h = if let Some(height) = self.height {
4636 height
4637 } else {
4638 child_size.height.clamp(
4639 self.min_height.unwrap_or(0.0),
4640 self.max_height.unwrap_or(f32::INFINITY),
4641 )
4642 };
4643 Size {
4644 width: w,
4645 height: h,
4646 }
4647 }
4648
4649 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4651 self.render(renderer, rect);
4652 let child_proposal =
4653 self.transform_proposal(SizeProposal::new(Some(rect.width), Some(rect.height)));
4654 let child_size = view.intrinsic_size(renderer, child_proposal);
4655
4656 let mut child_x = rect.x;
4657 let mut child_y = rect.y;
4658
4659 match self.alignment {
4660 Alignment::Leading => {
4661 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4662 }
4663 Alignment::Trailing => {
4664 child_x = rect.x + rect.width - child_size.width;
4665 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4666 }
4667 Alignment::Top => {
4668 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4669 }
4670 Alignment::Bottom => {
4671 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4672 child_y = rect.y + rect.height - child_size.height;
4673 }
4674 Alignment::Center => {
4675 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4676 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4677 }
4678 }
4679
4680 let child_rect = Rect {
4681 x: child_x,
4682 y: child_y,
4683 width: child_size.width,
4684 height: child_size.height,
4685 };
4686
4687 view.render(renderer, child_rect);
4688 self.post_render(renderer, rect);
4689 }
4690}
4691
4692#[derive(Debug, Clone, Copy, PartialEq)]
4694pub struct FlexModifier {
4695 pub weight: f32,
4696}
4697
4698impl ViewModifier for FlexModifier {
4699 fn modify<V: View>(self, content: V) -> impl View {
4700 ModifiedView::new(content, self)
4701 }
4702
4703 fn child_flex_weight<V: View>(&self, _view: &V) -> f32 {
4704 self.weight
4705 }
4706}
4707
4708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4710pub struct GridPlacementModifier {
4711 pub placement: GridPlacement,
4713}
4714
4715impl ViewModifier for GridPlacementModifier {
4716 fn modify<V: View>(self, content: V) -> impl View {
4718 ModifiedView::new(content, self)
4719 }
4720
4721 fn get_grid_placement(&self) -> Option<GridPlacement> {
4723 Some(self.placement)
4724 }
4725}
4726
4727#[derive(Clone)]
4730pub struct OverlayModifier {
4731 pub overlay: AnyView,
4733 pub alignment: Alignment,
4735 pub offset: [f32; 2],
4737 pub on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
4739}
4740
4741impl ViewModifier for OverlayModifier {
4742 fn modify<V: View>(self, content: V) -> impl View {
4744 ModifiedView::new(content, self)
4745 }
4746
4747 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4749 view.render(renderer, rect);
4751
4752 let overlay_size = self
4754 .overlay
4755 .intrinsic_size(renderer, SizeProposal::unspecified());
4756
4757 let mut overlay_x;
4759 let mut overlay_y;
4760
4761 match self.alignment {
4762 Alignment::Leading => {
4763 overlay_x = rect.x - overlay_size.width;
4764 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4765 }
4766 Alignment::Trailing => {
4767 overlay_x = rect.x + rect.width;
4768 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4769 }
4770 Alignment::Top => {
4771 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4772 overlay_y = rect.y - overlay_size.height;
4773 }
4774 Alignment::Bottom => {
4775 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4776 overlay_y = rect.y + rect.height;
4777 }
4778 Alignment::Center => {
4779 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4780 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4781 }
4782 }
4783
4784 overlay_x += self.offset[0];
4785 overlay_y += self.offset[1];
4786
4787 let overlay_rect = Rect {
4788 x: overlay_x,
4789 y: overlay_y,
4790 width: overlay_size.width,
4791 height: overlay_size.height,
4792 };
4793
4794 if let Some(on_dismiss) = &self.on_dismiss {
4796 let dismiss = on_dismiss.clone();
4797 renderer.register_handler(
4798 "pointerdown",
4799 Arc::new(move |event| {
4800 if let Event::PointerDown { x, y, .. } = event {
4801 let click_inside = x >= overlay_rect.x
4802 && x <= overlay_rect.x + overlay_rect.width
4803 && y >= overlay_rect.y
4804 && y <= overlay_rect.y + overlay_rect.height;
4805 if !click_inside {
4806 dismiss();
4807 }
4808 }
4809 }),
4810 );
4811 }
4812
4813 self.overlay.render(renderer, overlay_rect);
4815 }
4816}
4817
4818#[derive(Debug, Clone, Copy, PartialEq)]
4820pub struct OffsetModifier {
4821 pub x: f32,
4822 pub y: f32,
4823}
4824
4825impl OffsetModifier {
4826 pub fn new(x: f32, y: f32) -> Self {
4827 Self { x, y }
4828 }
4829}
4830
4831impl ViewModifier for OffsetModifier {
4832 fn modify<V: View>(self, content: V) -> impl View {
4833 ModifiedView::new(content, self)
4834 }
4835}
4836
4837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4839pub struct ZIndexModifier {
4840 pub z_index: i32,
4841}
4842
4843impl ZIndexModifier {
4844 pub fn new(z_index: i32) -> Self {
4845 Self { z_index }
4846 }
4847}
4848
4849impl ViewModifier for ZIndexModifier {
4850 fn modify<V: View>(self, content: V) -> impl View {
4851 ModifiedView::new(content, self)
4852 }
4853}
4854
4855#[derive(Debug, Clone, Copy, PartialEq, Default)]
4857pub struct LayoutConstraints {
4858 pub min_width: Option<f32>,
4859 pub max_width: Option<f32>,
4860 pub min_height: Option<f32>,
4861 pub max_height: Option<f32>,
4862}
4863
4864#[derive(Debug, Clone, Copy, PartialEq)]
4866pub struct LayoutModifier {
4867 pub constraints: LayoutConstraints,
4868}
4869
4870impl LayoutModifier {
4871 pub fn new(constraints: LayoutConstraints) -> Self {
4872 Self { constraints }
4873 }
4874}
4875
4876impl ViewModifier for LayoutModifier {
4877 fn modify<V: View>(self, content: V) -> impl View {
4878 ModifiedView::new(content, self)
4879 }
4880}
4881
4882#[derive(Debug, Clone, Copy, PartialEq)]
4884pub struct SafeAreaModifier {
4885 pub ignores: bool,
4886}
4887
4888impl ViewModifier for SafeAreaModifier {
4889 fn modify<V: View>(self, content: V) -> impl View {
4890 ModifiedView::new(content, self)
4891 }
4892}
4893
4894#[derive(Debug, Clone, Copy, PartialEq)]
4896pub struct ElevationModifier {
4897 pub level: f32,
4898}
4899
4900impl ViewModifier for ElevationModifier {
4901 fn modify<V: View>(self, content: V) -> impl View {
4902 ModifiedView::new(content, self)
4903 }
4904
4905 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4906 if self.level > 0.0 {
4907 let radius = self.level * 2.0;
4908 let offset_y = self.level * 0.5;
4909 let shadow_color = [0.0, 0.0, 0.0, 0.3];
4910 renderer.push_shadow(radius, shadow_color, [0.0, offset_y]);
4911 view.render(renderer, rect);
4912 renderer.pop_shadow();
4913 } else {
4914 view.render(renderer, rect);
4915 }
4916 }
4917}
4918
4919#[derive(Clone)]
4922pub struct PositionModifier {
4923 pub x: f32,
4924 pub y: f32,
4925}
4926
4927impl ViewModifier for PositionModifier {
4928 fn modify<V: View>(self, content: V) -> impl View {
4929 ModifiedView::new(content, self)
4930 }
4931
4932 fn transform_rect(&self, rect: Rect) -> Rect {
4933 Rect {
4934 x: rect.x + self.x,
4935 y: rect.y + self.y,
4936 width: rect.width,
4937 height: rect.height,
4938 }
4939 }
4940}
4941
4942pub mod layout {
4944 use super::*;
4945
4946 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4949 pub struct LayoutKey {
4950 pub view_hash: u64,
4951 pub generation: u64,
4952 }
4953
4954 pub struct LayoutCache {
4956 pub safe_area: SafeArea,
4957 pub delta_time: f32,
4958 pub scale_factor: f32,
4960 pub viewport: Option<Rect>,
4963 pub layout_time_budget: std::time::Duration,
4965 pub layout_start_time: Option<std::time::Instant>,
4967 size_cache: HashMap<(u64, u32, u32), Size>, pub parent_map: HashMap<u64, u64>,
4970 generation: u64,
4974 pub engine: Option<Box<dyn std::any::Any + Send + Sync>>,
4976 pub animators: Option<Box<dyn std::any::Any + Send + Sync>>,
4978 pub previous_rects: HashMap<u64, Rect>,
4980 pub eviction_generation: u64,
4983 pub previous_rects_generation: HashMap<u64, u64>,
4985 eviction_threshold: u64,
4987 }
4988
4989 thread_local! {
4990 static LAYOUT_BUDGET_DEADLINE: std::cell::RefCell<Option<std::time::Instant>> =
4991 const { std::cell::RefCell::new(None) };
4992 }
4993
4994 impl Default for LayoutCache {
4995 fn default() -> Self {
4996 Self::new()
4997 }
4998 }
4999
5000 impl LayoutCache {
5001 pub fn new() -> Self {
5002 Self {
5003 safe_area: SafeArea::default(),
5004 delta_time: 0.016,
5005 scale_factor: 1.0,
5006 viewport: None,
5007 layout_time_budget: std::time::Duration::from_millis(4),
5008 layout_start_time: None,
5009 size_cache: HashMap::new(),
5010 parent_map: HashMap::new(),
5011 generation: 0,
5012 engine: None,
5013 animators: None,
5014 previous_rects: HashMap::new(),
5015 eviction_generation: 0,
5016 previous_rects_generation: HashMap::new(),
5017 eviction_threshold: 300, }
5019 }
5020
5021 pub fn generation(&self) -> u64 {
5023 self.generation
5024 }
5025
5026 pub fn evict_stale_entries(&mut self) {
5028 self.eviction_generation += 1;
5029 let threshold = self.eviction_threshold;
5030 let current_gen = self.eviction_generation;
5031 self.previous_rects.retain(|hash, _| {
5032 self.previous_rects_generation
5033 .get(hash)
5034 .map_or(false, |g| current_gen - *g < threshold)
5035 });
5036 self.previous_rects_generation
5037 .retain(|hash, _| self.previous_rects.contains_key(hash));
5038 }
5039
5040 pub fn is_over_budget(&self) -> bool {
5042 let deadline_red = LAYOUT_BUDGET_DEADLINE.with(|deadline| {
5043 deadline.borrow().as_ref().is_some_and(|deadline| std::time::Instant::now() >= *deadline)
5044 });
5045 if deadline_red {
5046 return true;
5047 }
5048 if let Some(start) = self.layout_start_time {
5049 start.elapsed() > self.layout_time_budget
5050 } else {
5051 false
5052 }
5053 }
5054
5055 pub fn set_layout_budget_deadline(deadline: Option<std::time::Instant>) {
5059 LAYOUT_BUDGET_DEADLINE.with(|slot| {
5060 *slot.borrow_mut() = deadline;
5061 });
5062 }
5063
5064 pub fn clear_layout_budget_deadline() {
5066 Self::set_layout_budget_deadline(None);
5067 }
5068
5069 pub fn invalidate(&mut self) {
5073 self.generation = self.generation.wrapping_add(1);
5074 }
5075
5076 pub fn is_valid(&self, key: LayoutKey, current_gen: u64) -> bool {
5079 key.generation == current_gen && key.generation == self.generation
5080 }
5081
5082 pub fn clear(&mut self) {
5083 self.safe_area = SafeArea::default();
5084 self.viewport = None;
5085 self.layout_start_time = None;
5086 self.size_cache.clear();
5087 self.parent_map.clear();
5088 }
5089
5090 pub fn get_size(&self, view_hash: u64, proposal: SizeProposal) -> Option<Size> {
5091 let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5092 let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5093 self.size_cache.get(&(view_hash, pw, ph)).copied()
5094 }
5095
5096 pub fn set_size(&mut self, view_hash: u64, proposal: SizeProposal, size: Size) {
5097 let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5098 let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5099 self.size_cache.insert((view_hash, pw, ph), size);
5100 }
5101
5102 pub fn register_parent(&mut self, child_hash: u64, parent_hash: u64) {
5104 if child_hash != 0 && parent_hash != 0 {
5105 self.parent_map.insert(child_hash, parent_hash);
5106 }
5107 }
5108
5109 pub fn invalidate_view(&mut self, view_hash: u64) {
5112 let mut to_invalidate = vec![view_hash];
5113 let mut visited = std::collections::HashSet::new();
5114 while let Some(hash) = to_invalidate.pop() {
5115 if !visited.insert(hash) {
5116 continue;
5117 }
5118 self.size_cache.retain(|&(h, _, _), _| h != hash);
5119 if let Some(&parent) = self.parent_map.get(&hash) {
5120 to_invalidate.push(parent);
5121 }
5122 }
5123 }
5124 }
5125
5126 #[derive(Debug, Clone, Copy, PartialEq)]
5128 pub struct SizeProposal {
5129 pub width: Option<f32>,
5130 pub height: Option<f32>,
5131 }
5132
5133 impl SizeProposal {
5134 pub fn unspecified() -> Self {
5135 Self {
5136 width: None,
5137 height: None,
5138 }
5139 }
5140
5141 pub fn width(width: f32) -> Self {
5142 Self {
5143 width: Some(width),
5144 height: None,
5145 }
5146 }
5147
5148 pub fn height(height: f32) -> Self {
5149 Self {
5150 width: None,
5151 height: Some(height),
5152 }
5153 }
5154
5155 pub fn tight(width: f32, height: f32) -> Self {
5156 Self {
5157 width: Some(width),
5158 height: Some(height),
5159 }
5160 }
5161
5162 pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
5163 Self { width, height }
5164 }
5165 }
5166
5167 pub trait LayoutView: Send {
5169 fn size_that_fits(
5171 &self,
5172 proposal: SizeProposal,
5173 subviews: &[&dyn LayoutView],
5174 cache: &mut LayoutCache,
5175 ) -> Size;
5176
5177 fn place_subviews(
5179 &self,
5180 bounds: Rect,
5181 subviews: &mut [&mut dyn LayoutView],
5182 cache: &mut LayoutCache,
5183 );
5184
5185 fn flex_weight(&self) -> f32 {
5187 0.0
5188 }
5189
5190 fn view_hash(&self) -> u64 {
5193 0
5194 }
5195
5196 fn changed(&self) -> bool {
5205 true
5206 }
5207
5208 fn debug_layout(&self, indent: usize) -> String {
5211 let prefix = " ".repeat(indent);
5212 format!("{}LayoutView", prefix)
5213 }
5214 }
5215 #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5217 pub struct EdgeInsets {
5218 pub top: f32,
5219 pub leading: f32,
5220 pub bottom: f32,
5221 pub trailing: f32,
5222 }
5223
5224 impl EdgeInsets {
5225 pub fn new(top: f32, leading: f32, bottom: f32, trailing: f32) -> Self {
5226 Self {
5227 top,
5228 leading,
5229 bottom,
5230 trailing,
5231 }
5232 }
5233
5234 pub fn all(value: f32) -> Self {
5235 Self {
5236 top: value,
5237 leading: value,
5238 bottom: value,
5239 trailing: value,
5240 }
5241 }
5242 }
5243
5244 #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5246 pub struct SafeArea {
5247 pub insets: EdgeInsets,
5248 }
5249
5250 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5252 pub enum SdfShape {
5253 Rect(Rect),
5254 RoundedRect { rect: Rect, radius: f32 },
5255 Circle { center: [f32; 2], radius: f32 },
5256 }
5257
5258 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5260 pub struct Rect {
5261 pub x: f32,
5262 pub y: f32,
5263 pub width: f32,
5264 pub height: f32,
5265 }
5266
5267 impl Rect {
5268 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
5269 Self {
5270 x,
5271 y,
5272 width,
5273 height,
5274 }
5275 }
5276
5277 pub fn inset(&self, amount: f32) -> Self {
5278 Self {
5279 x: self.x + amount,
5280 y: self.y + amount,
5281 width: (self.width - amount * 2.0).max(0.0),
5282 height: (self.height - amount * 2.0).max(0.0),
5283 }
5284 }
5285
5286 pub fn offset(&self, dx: f32, dy: f32) -> Self {
5287 Self {
5288 x: self.x + dx,
5289 y: self.y + dy,
5290 ..*self
5291 }
5292 }
5293
5294 pub fn zero() -> Self {
5295 Self {
5296 x: 0.0,
5297 y: 0.0,
5298 width: 0.0,
5299 height: 0.0,
5300 }
5301 }
5302
5303 pub fn contains(&self, x: f32, y: f32) -> bool {
5304 x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
5305 }
5306
5307 pub fn intersects(&self, other: &Rect) -> bool {
5314 self.x < other.x + other.width
5315 && self.x + self.width > other.x
5316 && self.y < other.y + other.height
5317 && self.y + self.height > other.y
5318 }
5319
5320 pub fn size(&self) -> Size {
5321 Size {
5322 width: self.width,
5323 height: self.height,
5324 }
5325 }
5326
5327 pub fn split_horizontal(&self, n: usize) -> Vec<Rect> {
5329 if n == 0 {
5330 return vec![];
5331 }
5332 let item_width = self.width / n as f32;
5333 (0..n)
5334 .map(|i| Rect {
5335 x: self.x + i as f32 * item_width,
5336 y: self.y,
5337 width: item_width,
5338 height: self.height,
5339 })
5340 .collect()
5341 }
5342
5343 pub fn split_vertical(&self, n: usize) -> Vec<Rect> {
5345 if n == 0 {
5346 return vec![];
5347 }
5348 let item_height = self.height / n as f32;
5349 (0..n)
5350 .map(|i| Rect {
5351 x: self.x,
5352 y: self.y + i as f32 * item_height,
5353 width: self.width,
5354 height: item_height,
5355 })
5356 .collect()
5357 }
5358 }
5359}
5360
5361pub mod agents;
5364pub mod animation;
5365pub mod gpu;
5366pub mod material;
5367pub mod runtime;
5368pub mod scene_graph;
5369pub mod sdf_shadow;
5370
5371pub use layout::{LayoutCache, LayoutKey, LayoutView, Rect, SizeProposal};
5373pub use material::DrawMaterial;
5374pub use scene_graph::{NodeId, bifrost_registry};
5375pub use color::SemanticColors;
5376
5377pub trait AssetManager: Send + Sync {
5381 fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>>;
5383
5384 fn preload_image(&self, url: &str);
5386}
5387
5388#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5390pub enum TouchPhase {
5391 Began,
5393 Moved,
5395 Ended,
5397 Cancelled,
5399}
5400
5401#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
5403pub enum Event {
5404 PointerDown {
5405 x: f32,
5406 y: f32,
5407 button: u32,
5408 proximity_field: f32,
5409 tilt: Option<f32>,
5410 azimuth: Option<f32>,
5411 pressure: Option<f32>,
5412 barrel_rotation: Option<f32>,
5413 pointer_precision: f32,
5414 },
5415 PointerUp {
5416 x: f32,
5417 y: f32,
5418 button: u32,
5419 tilt: Option<f32>,
5420 azimuth: Option<f32>,
5421 pressure: Option<f32>,
5422 barrel_rotation: Option<f32>,
5423 pointer_precision: f32,
5424 },
5425 PointerMove {
5426 x: f32,
5427 y: f32,
5428 proximity_field: f32,
5429 tilt: Option<f32>,
5430 azimuth: Option<f32>,
5431 pressure: Option<f32>,
5432 barrel_rotation: Option<f32>,
5433 pointer_precision: f32,
5434 },
5435 PointerClick {
5436 x: f32,
5437 y: f32,
5438 button: u32,
5439 tilt: Option<f32>,
5440 azimuth: Option<f32>,
5441 pressure: Option<f32>,
5442 barrel_rotation: Option<f32>,
5443 pointer_precision: f32,
5444 },
5445 PointerEnter,
5446 PointerLeave,
5447 PointerWheel {
5450 x: f32,
5451 y: f32,
5452 delta_x: f32,
5453 delta_y: f32,
5454 pointer_precision: f32,
5455 },
5456 PointerDoubleClick {
5458 x: f32,
5459 y: f32,
5460 button: u32,
5461 pointer_precision: f32,
5462 },
5463 DragStart {
5465 x: f32,
5466 y: f32,
5467 button: u32,
5468 pointer_precision: f32,
5469 },
5470 DragMove {
5472 x: f32,
5473 y: f32,
5474 pointer_precision: f32,
5475 },
5476 DragEnd {
5478 x: f32,
5479 y: f32,
5480 pointer_precision: f32,
5481 },
5482 KeyDown {
5483 key: String,
5484 modifiers: KeyModifiers,
5485 },
5486 KeyUp {
5487 key: String,
5488 modifiers: KeyModifiers,
5489 },
5490 FocusIn,
5492 FocusOut,
5494 Copy,
5496 Cut,
5498 Paste(String),
5500 Ime(String),
5502 TouchStart {
5504 x: f32,
5505 y: f32,
5506 touch_id: u64,
5507 },
5508 TouchMove {
5510 x: f32,
5511 y: f32,
5512 touch_id: u64,
5513 },
5514 TouchEnd {
5516 x: f32,
5517 y: f32,
5518 touch_id: u64,
5519 },
5520 TouchCancel {
5522 touch_id: u64,
5523 },
5524 GesturePinch {
5530 center: [f32; 2],
5531 scale: f32,
5532 velocity: f32,
5533 phase: TouchPhase,
5534 },
5535 GestureSwipe {
5540 direction: [f32; 2],
5541 velocity: f32,
5542 phase: TouchPhase,
5543 },
5544 FileDrop {
5546 x: f32,
5547 y: f32,
5548 path: String,
5549 },
5550}
5551
5552impl Event {
5553 pub fn pointer_precision(&self) -> f32 {
5559 match self {
5560 Self::PointerDown {
5561 pointer_precision, ..
5562 }
5563 | Self::PointerUp {
5564 pointer_precision, ..
5565 }
5566 | Self::PointerMove {
5567 pointer_precision, ..
5568 }
5569 | Self::PointerClick {
5570 pointer_precision, ..
5571 }
5572 | Self::PointerWheel {
5573 pointer_precision, ..
5574 }
5575 | Self::PointerDoubleClick {
5576 pointer_precision, ..
5577 }
5578 | Self::DragStart {
5579 pointer_precision, ..
5580 }
5581 | Self::DragMove {
5582 pointer_precision, ..
5583 }
5584 | Self::DragEnd {
5585 pointer_precision, ..
5586 } => *pointer_precision,
5587 _ => 0.0,
5588 }
5589 }
5590
5591 pub fn name(&self) -> &'static str {
5593 match self {
5594 Self::PointerDown { .. } => "pointerdown",
5595 Self::PointerUp { .. } => "pointerup",
5596 Self::PointerMove { .. } => "pointermove",
5597 Self::PointerClick { .. } => "pointerclick",
5598 Self::PointerEnter => "pointerenter",
5599 Self::PointerLeave => "pointerleave",
5600 Self::PointerWheel { .. } => "pointerwheel",
5601 Self::PointerDoubleClick { .. } => "pointerdoubleclick",
5602 Self::DragStart { .. } => "dragstart",
5603 Self::DragMove { .. } => "dragmove",
5604 Self::DragEnd { .. } => "dragend",
5605 Self::KeyDown { .. } => "keydown",
5606 Self::KeyUp { .. } => "keyup",
5607 Self::FocusIn => "focusin",
5608 Self::FocusOut => "focusout",
5609 Self::Copy => "copy",
5610 Self::Cut => "cut",
5611 Self::Paste(_) => "paste",
5612 Self::Ime(_) => "ime",
5613 Self::TouchStart { .. } => "touchstart",
5614 Self::TouchMove { .. } => "touchmove",
5615 Self::TouchEnd { .. } => "touchend",
5616 Self::TouchCancel { .. } => "touchcancel",
5617 Self::GesturePinch { .. } => "gesturepinch",
5618 Self::GestureSwipe { .. } => "gestureswipe",
5619 Self::FileDrop { .. } => "filedrop",
5620 }
5621 }
5622}
5623
5624
5625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5627pub enum EventResponse {
5628 Handled,
5629 Ignored,
5630}
5631
5632#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5663pub enum EventPhase {
5664 Capture,
5666 Target,
5668 Bubble,
5670}
5671
5672impl EventPhase {
5673 pub const ALL: [EventPhase; 3] = [
5675 EventPhase::Capture,
5676 EventPhase::Target,
5677 EventPhase::Bubble,
5678 ];
5679}
5680
5681pub struct DefaultAssetManager {
5683 cache: AssetCache,
5684}
5685type AssetCache = Arc<arc_swap::ArcSwap<HashMap<String, AssetState<Arc<Vec<u8>>>>>>;
5686
5687impl Default for DefaultAssetManager {
5688 fn default() -> Self {
5689 Self::new()
5690 }
5691}
5692
5693impl DefaultAssetManager {
5694 pub fn new() -> Self {
5695 Self {
5696 cache: Arc::new(arc_swap::ArcSwap::from_pointee(HashMap::new())),
5697 }
5698 }
5699}
5700
5701impl AssetManager for DefaultAssetManager {
5702 fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>> {
5703 if let Some(state) = self.cache.load().get(url) {
5704 return state.clone();
5705 }
5706
5707 self.cache.rcu(|map| {
5708 let mut m = (**map).clone();
5709 m.entry(url.to_string()).or_insert(AssetState::Loading);
5710 m
5711 });
5712 AssetState::Loading
5713 }
5714
5715 fn preload_image(&self, _url: &str) {}
5716}
5717
5718use std::future::Future;
5719
5720pub struct Suspense<T: Clone + Send + Sync + 'static> {
5723 inner: State<AssetState<T>>,
5724}
5725
5726impl<T: Clone + Send + Sync + 'static> Default for Suspense<T> {
5727 fn default() -> Self {
5728 Self::new()
5729 }
5730}
5731
5732impl<T: Clone + Send + Sync + 'static> Suspense<T> {
5733 pub fn new() -> Self {
5734 Self {
5735 inner: State::new(AssetState::Loading),
5736 }
5737 }
5738
5739 pub fn new_async<F>(future: F) -> Self
5740 where
5741 F: Future<Output = Result<T, String>> + Send + 'static,
5742 {
5743 let suspense = Self::new();
5744 let suspense_clone = suspense.clone();
5745
5746 #[cfg(not(target_arch = "wasm32"))]
5747 {
5748 if let Ok(handle) = tokio::runtime::Handle::try_current() {
5754 handle.spawn(async move {
5755 let result = future.await;
5756 match result {
5757 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5758 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5759 }
5760 });
5761 } else {
5762 fallback_runtime().spawn(async move {
5763 let result = future.await;
5764 match result {
5765 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5766 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5767 }
5768 });
5769 }
5770 }
5771 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
5772 {
5773 wasm_bindgen_futures::spawn_local(async move {
5774 let result = future.await;
5775 match result {
5776 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5777 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5778 }
5779 });
5780 }
5781
5782 suspense
5783 }
5784
5785 pub fn ready(value: T) -> Self {
5786 Self {
5787 inner: State::new(AssetState::Ready(value)),
5788 }
5789 }
5790
5791 pub fn error(message: impl Into<String>) -> Self {
5792 Self {
5793 inner: State::new(AssetState::Error(message.into())),
5794 }
5795 }
5796
5797 pub fn get(&self) -> AssetState<T> {
5798 self.inner.get()
5799 }
5800
5801 pub fn get_ref(&self) -> AssetState<T> {
5802 self.inner.get()
5803 }
5804
5805 pub fn is_loading(&self) -> bool {
5806 matches!(self.get(), AssetState::Loading)
5807 }
5808
5809 pub fn is_ready(&self) -> bool {
5810 matches!(self.get(), AssetState::Ready(_))
5811 }
5812
5813 pub fn is_error(&self) -> bool {
5814 matches!(self.get(), AssetState::Error(_))
5815 }
5816
5817 pub fn ready_value(&self) -> Option<T> {
5818 match self.get() {
5819 AssetState::Ready(value) => Some(value),
5820 _ => None,
5821 }
5822 }
5823
5824 pub fn error_message(&self) -> Option<String> {
5825 match self.get() {
5826 AssetState::Error(message) => Some(message),
5827 _ => None,
5828 }
5829 }
5830
5831 pub fn subscribe<F: Fn(&AssetState<T>) + Send + Sync + 'static>(&self, callback: F) {
5832 self.inner.subscribe(callback)
5833 }
5834
5835 pub fn inner_state(&self) -> &State<AssetState<T>> {
5836 &self.inner
5837 }
5838}
5839
5840impl<T: Clone + Send + Sync + 'static> Clone for Suspense<T> {
5841 fn clone(&self) -> Self {
5842 Self {
5843 inner: self.inner.clone(),
5844 }
5845 }
5846}
5847
5848impl<T: Clone + Send + Sync + 'static> From<T> for Suspense<T> {
5849 fn from(value: T) -> Self {
5850 Self::ready(value)
5851 }
5852}
5853
5854impl<T: Clone + Send + Sync + 'static> From<Result<T, String>> for Suspense<T> {
5855 fn from(result: Result<T, String>) -> Self {
5856 match result {
5857 Ok(value) => Self::ready(value),
5858 Err(error) => Self::error(error),
5859 }
5860 }
5861}
5862
5863#[cfg(test)]
5864mod phase1_test;
5865
5866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5868pub enum RenderIntensityMode {
5869 Normal,
5870 Rage, Frenzy, GodMode, }
5874
5875pub trait Seer: Send + Sync {
5878 fn predict(&self, context: &str) -> String;
5880 fn whispers(&self) -> Vec<String>;
5882}
5883
5884#[cfg(test)]
5885mod vili_tests {
5886 use super::*;
5887
5888 struct DummyRenderer;
5889 impl ElapsedTime for DummyRenderer {
5890 fn elapsed_time(&self) -> f32 {
5891 0.0
5892 }
5893 fn delta_time(&self) -> f32 {
5894 0.0
5895 }
5896 }
5897 impl Renderer for DummyRenderer {
5898 fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
5899 fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
5900 fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
5901 fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5902 fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
5903 fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5904 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
5905
5906 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
5907 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
5908 }
5909 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
5910 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
5911 fn pop_transform_3d(&mut self) {}
5912 }
5913
5914 #[test]
5915 fn test_magnetic_warp() {
5916 let renderer = DummyRenderer;
5917 let anchor = Rect {
5918 x: 100.0,
5919 y: 100.0,
5920 width: 50.0,
5921 height: 50.0,
5922 };
5923 let pointer = [125.0, 50.0];
5925 let warp = renderer.magnetic_warp(pointer, anchor, 1.0);
5928 assert!(warp[1] > 50.0);
5930
5931 let far_pointer = [500.0, 500.0];
5933 let far_warp = renderer.magnetic_warp(far_pointer, anchor, 1.0);
5934 assert_eq!(far_pointer, far_warp);
5935 }
5936
5937 #[test]
5938 fn test_mani_glow() {
5939 let renderer = DummyRenderer;
5940 let bounds = Rect {
5941 x: 0.0,
5942 y: 0.0,
5943 width: 100.0,
5944 height: 100.0,
5945 };
5946 let pointer_inside = [50.0, 50.0];
5947 let glow_max = renderer.mani_glow_intensity(pointer_inside, bounds, 120.0);
5948 assert_eq!(glow_max, 1.0);
5949
5950 let pointer_edge = [50.0, -10.0];
5951 let glow_partial = renderer.mani_glow_intensity(pointer_edge, bounds, 120.0);
5952 assert!(glow_partial > 0.0 && glow_partial < 1.0);
5953 }
5954
5955 #[test]
5956 fn test_fafnir_evolve() {
5957 let renderer = DummyRenderer;
5958 let bounds = Rect {
5959 x: 0.0,
5960 y: 0.0,
5961 width: 100.0,
5962 height: 100.0,
5963 };
5964 let pointer_inside = [50.0, 50.0];
5965 let scale = renderer.fafnir_evolve(pointer_inside, bounds, 1.2);
5966 assert_eq!(scale, 1.2); }
5968
5969 #[test]
5970 fn test_undo_manager_basic() {
5971 let mut manager = UndoManager::new(3, 0.5);
5972 let val = std::sync::Arc::new(std::sync::Mutex::new(0));
5973
5974 let v1 = val.clone();
5975 let v2 = val.clone();
5976 manager.push(
5977 "Add",
5978 move || *v1.lock().unwrap() -= 1,
5979 move || *v2.lock().unwrap() += 1,
5980 );
5981
5982 assert!(manager.can_undo());
5983 assert!(!manager.can_redo());
5984
5985 let undo = manager.undo().unwrap();
5986 undo();
5987 assert_eq!(*val.lock().unwrap(), -1);
5988 assert!(!manager.can_undo());
5989 assert!(manager.can_redo());
5990
5991 let redo = manager.redo().unwrap();
5992 redo();
5993 assert_eq!(*val.lock().unwrap(), 0);
5994 }
5995
5996 #[test]
5997 fn test_undo_manager_depth_limit() {
5998 let mut manager = UndoManager::new(2, 0.5);
5999 manager.push("1", || {}, || {});
6000 manager.push("2", || {}, || {});
6001 manager.push("3", || {}, || {});
6002
6003 assert_eq!(manager.stack.len(), 2);
6004 assert_eq!(manager.position, 2);
6005 }
6006
6007 #[test]
6008 fn test_undo_manager_coalescing() {
6009 let mut manager = UndoManager::new(10, 1.0);
6010 let count = std::sync::Arc::new(std::sync::Mutex::new(0));
6011
6012 let c = count.clone();
6013 manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
6014
6015 let c = count.clone();
6016 manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
6017
6018 assert_eq!(manager.stack.len(), 1);
6019
6020 let undo = manager.undo().unwrap();
6021 undo();
6022 assert_eq!(*count.lock().unwrap(), -2);
6023 }
6024}
6025
6026#[cfg(test)]
6027mod error_boundary_tests {
6028 use super::*;
6029
6030 struct SuccessView;
6032
6033 impl View for SuccessView {
6034 type Body = Never;
6035 fn body(self) -> Never {
6036 unreachable!()
6038 }
6039 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6040 }
6042 }
6043
6044 struct PanicOnRender;
6046
6047 impl View for PanicOnRender {
6048 type Body = Never;
6049 fn body(self) -> Never {
6050 unreachable!()
6052 }
6053 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6054 panic!("intentional render panic");
6055 }
6056 }
6057
6058 struct PanicOnSize;
6060
6061 impl View for PanicOnSize {
6062 type Body = Never;
6063 fn body(self) -> Never {
6064 unreachable!()
6066 }
6067 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6068 }
6070 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
6071 panic!("intentional size panic");
6072 }
6073 }
6074
6075 struct PanicWithString;
6077
6078 impl View for PanicWithString {
6079 type Body = Never;
6080 fn body(self) -> Never {
6081 unreachable!()
6083 }
6084 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6085 panic!("{}", "custom error message".to_string());
6086 }
6087 }
6088
6089 struct DummyRenderer;
6090 impl ElapsedTime for DummyRenderer {
6091 fn elapsed_time(&self) -> f32 {
6092 0.0
6093 }
6094 fn delta_time(&self) -> f32 {
6095 0.0
6096 }
6097 }
6098 impl Renderer for DummyRenderer {
6099 fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
6100 fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
6101 fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
6102 fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6103 fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
6104 fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6105 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6106
6107 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6108 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
6109 }
6110 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6111 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6112 fn pop_transform_3d(&mut self) {}
6113 }
6114
6115 const TEST_RECT: Rect = Rect {
6116 x: 0.0,
6117 y: 0.0,
6118 width: 200.0,
6119 height: 100.0,
6120 };
6121
6122 #[test]
6123 fn error_boundary_renders_child_on_success() {
6124 let boundary = ErrorBoundary::new(SuccessView);
6125 let mut renderer = DummyRenderer;
6126
6127 boundary.render(&mut renderer, TEST_RECT);
6128
6129 assert!(
6130 !boundary.has_error(),
6131 "should not have error after successful render"
6132 );
6133 assert!(
6134 boundary.last_error().is_none(),
6135 "should have no error message"
6136 );
6137 }
6138
6139 #[test]
6140 fn error_boundary_catches_render_panic() {
6141 let boundary = ErrorBoundary::new(PanicOnRender);
6142 let mut renderer = DummyRenderer;
6143
6144 boundary.render(&mut renderer, TEST_RECT);
6146
6147 assert!(
6148 boundary.has_error(),
6149 "should have error after catching panic"
6150 );
6151 let err = boundary.last_error().expect("should have error message");
6152 assert!(
6153 err.contains("intentional render panic"),
6154 "error message should contain panic message, got: {err}"
6155 );
6156 }
6157
6158 #[test]
6159 fn error_boundary_catches_size_panic() {
6160 let boundary = ErrorBoundary::new(PanicOnSize);
6161 let mut renderer = DummyRenderer;
6162 let proposal = layout::SizeProposal {
6163 width: Some(100.0),
6164 height: Some(50.0),
6165 };
6166
6167 let size = boundary.intrinsic_size(&mut renderer, proposal);
6168
6169 assert!(
6170 boundary.has_error(),
6171 "should have error after catching size panic"
6172 );
6173 assert_eq!(size, Size::ZERO, "fallback size should be zero");
6174 }
6175
6176 #[test]
6177 fn error_boundary_catches_string_panic() {
6178 let boundary = ErrorBoundary::new(PanicWithString);
6179 let mut renderer = DummyRenderer;
6180
6181 boundary.render(&mut renderer, TEST_RECT);
6182
6183 assert!(boundary.has_error());
6184 let err = boundary.last_error().expect("should have error message");
6185 assert!(
6186 err.contains("custom error message"),
6187 "should capture String panic payload, got: {err}"
6188 );
6189 }
6190
6191 #[test]
6192 fn error_boundary_clear_error_resets_state() {
6193 let boundary = ErrorBoundary::new(PanicOnRender);
6194 let mut renderer = DummyRenderer;
6195
6196 boundary.render(&mut renderer, TEST_RECT);
6197 assert!(boundary.has_error());
6198
6199 boundary.clear_error();
6200 assert!(
6201 !boundary.has_error(),
6202 "should be clear after clear_error()"
6203 );
6204 assert!(
6205 boundary.last_error().is_none(),
6206 "error message should be cleared"
6207 );
6208 }
6209
6210 #[test]
6211 fn error_boundary_fallback_color_is_configurable() {
6212 let boundary = ErrorBoundary::new(SuccessView)
6213 .fallback_color([0.0, 0.0, 1.0, 1.0])
6214 .fallback_label("custom label");
6215
6216 assert_eq!(boundary.fallback_color, [0.0, 0.0, 1.0, 1.0]);
6217 assert_eq!(
6218 boundary.fallback_label.as_deref(),
6219 Some("custom label")
6220 );
6221 }
6222
6223 #[test]
6224 fn error_boundary_flex_weight_delegates_to_child() {
6225 let boundary = ErrorBoundary::new(SuccessView);
6226 assert_eq!(boundary.flex_weight(), 0.0, "should delegate to child (default 0.0)");
6227 }
6228
6229 #[test]
6230 fn error_boundary_body_delegates_to_child() {
6231 let _boundary = ErrorBoundary::new(SuccessView);
6233 let _boundary_type = std::any::type_name::<ErrorBoundary<SuccessView>>();
6237 }
6238
6239 struct TrackingRenderer {
6242 clip_depth: u32,
6243 opacity_depth: u32,
6244 shadow_depth: u32,
6245 }
6246
6247 impl TrackingRenderer {
6248 fn new() -> Self {
6249 Self {
6250 clip_depth: 0,
6251 opacity_depth: 0,
6252 shadow_depth: 0,
6253 }
6254 }
6255 }
6256
6257 impl Renderer for TrackingRenderer {
6258 fn fill_rect(&mut self, _rect: Rect, _color: [f32; 4]) {}
6259 fn fill_rounded_rect(&mut self, _rect: Rect, _radius: f32, _color: [f32; 4]) {}
6260 fn fill_ellipse(&mut self, _rect: Rect, _color: [f32; 4]) {}
6261 fn stroke_rect(&mut self, _rect: Rect, _color: [f32; 4], _w: f32) {}
6262 fn stroke_rounded_rect(
6263 &mut self,
6264 _rect: Rect,
6265 _radius: f32,
6266 _color: [f32; 4],
6267 _stroke_width: f32,
6268 ) {
6269 }
6270 fn stroke_ellipse(&mut self, _rect: Rect, _color: [f32; 4], _stroke_width: f32) {}
6271 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6272
6273 fn push_clip_rect(&mut self, _rect: Rect) {
6274 self.clip_depth += 1;
6275 }
6276 fn pop_clip_rect(&mut self) {
6277 self.clip_depth = self.clip_depth.saturating_sub(1);
6278 }
6279 fn push_opacity(&mut self, _opacity: f32) {
6280 self.opacity_depth += 1;
6281 }
6282 fn pop_opacity(&mut self) {
6283 self.opacity_depth = self.opacity_depth.saturating_sub(1);
6284 }
6285 fn push_shadow(&mut self, _r: f32, _c: [f32; 4], _o: [f32; 2]) {
6286 self.shadow_depth += 1;
6287 }
6288 fn pop_shadow(&mut self) {
6289 self.shadow_depth = self.shadow_depth.saturating_sub(1);
6290 }
6291 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6292 fn snapshot_render_state(&self) -> RenderStateSnapshot {
6293 RenderStateSnapshot {
6296 clip_depth: self.clip_depth,
6297 opacity_depth: self.opacity_depth,
6298 slice_depth: 0,
6299 shadow_depth: self.shadow_depth,
6300 transform_depth: 0,
6301 vnode_depth: 0,
6302 }
6303 }
6304 fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
6305 self.clip_depth = snap.clip_depth;
6306 self.opacity_depth = snap.opacity_depth;
6307 self.shadow_depth = snap.shadow_depth;
6308 }
6309 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
6310 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6311 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6312 fn pop_transform_3d(&mut self) {}
6313 }
6314
6315 impl ElapsedTime for TrackingRenderer {
6316 fn elapsed_time(&self) -> f32 {
6317 0.0
6318 }
6319 fn delta_time(&self) -> f32 {
6320 0.0
6321 }
6322 }
6323
6324 struct StackPushingPanicView;
6328
6329 impl View for StackPushingPanicView {
6330 type Body = Never;
6331 fn body(self) -> Never {
6332 unreachable!()
6333 }
6334 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
6335 renderer.push_clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0));
6336 renderer.push_opacity(0.5);
6337 renderer.push_shadow(2.0, [0.0, 0.0, 0.0, 0.5], [0.0, 0.0]);
6338 panic!("intentional stack-pushing panic");
6339 }
6340 }
6341
6342 #[test]
6343 fn error_boundary_restores_renderer_state_on_panic() {
6344 let boundary = ErrorBoundary::new(StackPushingPanicView);
6348 let mut renderer = TrackingRenderer::new();
6349
6350 let snap_before = renderer.snapshot_render_state();
6352 assert_eq!(snap_before.clip_depth, 0);
6353 assert_eq!(snap_before.opacity_depth, 0);
6354 assert_eq!(snap_before.shadow_depth, 0);
6355
6356 boundary.render(&mut renderer, TEST_RECT);
6358
6359 assert!(boundary.has_error(), "should have caught the panic");
6361 let snap_after = renderer.snapshot_render_state();
6362 assert_eq!(
6363 snap_after.clip_depth, 0,
6364 "clip stack should be restored to empty after panic"
6365 );
6366 assert_eq!(
6367 snap_after.opacity_depth, 0,
6368 "opacity stack should be restored to empty after panic"
6369 );
6370 assert_eq!(
6371 snap_after.shadow_depth, 0,
6372 "shadow stack should be restored to empty after panic"
6373 );
6374 }
6375
6376 #[test]
6377 fn render_state_snapshot_default_is_zeroed() {
6378 let snap = RenderStateSnapshot::default();
6381 assert_eq!(snap.clip_depth, 0);
6382 assert_eq!(snap.opacity_depth, 0);
6383 assert_eq!(snap.slice_depth, 0);
6384 assert_eq!(snap.shadow_depth, 0);
6385 assert_eq!(snap.transform_depth, 0);
6386 assert_eq!(snap.vnode_depth, 0);
6387 }
6388
6389 #[test]
6390 fn render_state_snapshot_round_trip() {
6391 let snap = RenderStateSnapshot {
6392 clip_depth: 3,
6393 opacity_depth: 2,
6394 slice_depth: 1,
6395 shadow_depth: 0,
6396 transform_depth: 4,
6397 vnode_depth: 5,
6398 };
6399 let copied = snap;
6400 assert_eq!(copied, snap);
6401 }
6402}
6403
6404use std::cell::RefCell;
6416
6417thread_local! {
6418 static THEME_CONTEXT: RefCell<Option<ThemeContext>> = const { RefCell::new(None) };
6420}
6421
6422#[derive(Debug, Clone)]
6425pub struct ThemeContext {
6426 pub colors: color::SemanticColors,
6428 pub glassmorphism_enabled: bool,
6431}
6432
6433impl ThemeContext {
6434 pub fn dark() -> Self {
6436 Self {
6437 colors: color::SemanticColors::dark(),
6438 glassmorphism_enabled: true,
6439 }
6440 }
6441
6442 pub fn light() -> Self {
6444 Self {
6445 colors: color::SemanticColors::light(),
6446 glassmorphism_enabled: false,
6447 }
6448 }
6449}
6450
6451pub fn set_current_theme(colors: color::SemanticColors) {
6454 THEME_CONTEXT.with(|cell| {
6455 let is_light = (colors.background.r + colors.background.g + colors.background.b) / 3.0 > 0.5;
6456 let glassmorphism = !is_light; *cell.borrow_mut() = Some(ThemeContext { colors, glassmorphism_enabled: glassmorphism });
6458 });
6459}
6460
6461pub fn set_theme_context(ctx: ThemeContext) {
6463 THEME_CONTEXT.with(|cell| {
6464 *cell.borrow_mut() = Some(ctx);
6465 });
6466}
6467
6468pub fn clear_current_theme() {
6470 THEME_CONTEXT.with(|cell| {
6471 *cell.borrow_mut() = None;
6472 });
6473}
6474
6475pub fn use_theme() -> color::SemanticColors {
6490 THEME_CONTEXT.with(|cell| {
6491 cell.borrow()
6492 .clone()
6493 .map(|ctx| ctx.colors)
6494 .unwrap_or_else(color::SemanticColors::dark)
6495 })
6496}
6497
6498pub fn use_theme_context() -> ThemeContext {
6503 THEME_CONTEXT.with(|cell| {
6504 cell.borrow()
6505 .clone()
6506 .unwrap_or_else(ThemeContext::dark)
6507 })
6508}
6509
6510pub fn glassmorphism_enabled() -> bool {
6513 THEME_CONTEXT.with(|cell| {
6514 cell.borrow()
6515 .as_ref()
6516 .map(|ctx| ctx.glassmorphism_enabled)
6517 .unwrap_or(true) })
6519}
6520
6521pub mod color {
6530 use super::Color;
6531
6532 #[derive(Debug, Clone)]
6549 pub struct SemanticColors {
6550 pub primary: Color,
6552 pub secondary: Color,
6554 pub accent: Color,
6556 pub background: Color,
6558 pub surface: Color,
6560 pub error: Color,
6562 pub warning: Color,
6564 pub success: Color,
6566 pub text: Color,
6568 pub text_dim: Color,
6570 }
6571
6572 impl SemanticColors {
6573 pub fn dark() -> Self {
6575 Self {
6576 primary: Color::new(1.0, 0.84, 0.0, 1.0), secondary: Color::new(1.0, 0.0, 1.0, 1.0), accent: Color::new(1.0, 0.0, 0.4, 1.0), background: Color::new(0.02, 0.02, 0.05, 1.0), surface: Color::new(0.05, 0.05, 0.07, 1.0), error: Color::new(1.0, 0.2, 0.2, 1.0), warning: Color::new(1.0, 0.8, 0.0, 1.0), success: Color::new(0.0, 1.0, 0.5, 1.0), text: Color::new(0.95, 0.95, 1.0, 1.0), text_dim: Color::new(0.6, 0.6, 0.7, 1.0), }
6587 }
6588
6589 pub fn light() -> Self {
6591 Self {
6592 primary: Color::new(0.35, 0.30, 0.70, 1.0),
6593 secondary: Color::new(0.30, 0.50, 0.30, 1.0),
6594 accent: Color::new(0.30, 0.35, 0.75, 1.0),
6595 background: Color::new(0.97, 0.97, 0.98, 1.0),
6596 surface: Color::new(0.93, 0.93, 0.95, 1.0),
6597 error: Color::new(0.75, 0.15, 0.15, 1.0),
6598 warning: Color::new(0.80, 0.60, 0.0, 1.0),
6599 success: Color::new(0.15, 0.65, 0.30, 1.0),
6600 text: Color::new(0.08, 0.08, 0.10, 1.0),
6601 text_dim: Color::new(0.40, 0.40, 0.45, 1.0),
6602 }
6603 }
6604
6605 pub fn accent_states(&self) -> InteractiveColorStates {
6610 InteractiveColorStates::from_color(self.accent)
6611 }
6612
6613 pub fn primary_states(&self) -> InteractiveColorStates {
6615 InteractiveColorStates::from_color(self.primary)
6616 }
6617
6618 pub fn error_states(&self) -> InteractiveColorStates {
6620 InteractiveColorStates::from_color(self.error)
6621 }
6622
6623 pub fn success_states(&self) -> InteractiveColorStates {
6625 InteractiveColorStates::from_color(self.success)
6626 }
6627 }
6628
6629 #[derive(Debug, Clone)]
6634 pub struct InteractiveColorStates {
6635 pub default: Color,
6636 pub hover: Color,
6637 pub active: Color,
6638 pub focus: Color,
6639 pub disabled: Color,
6640 pub focus_ring: Color,
6641 }
6642
6643 impl InteractiveColorStates {
6644 pub fn from_color(base: Color) -> Self {
6653 Self {
6654 default: base,
6655 hover: base.lighten(0.15),
6656 active: base.darken(0.15),
6657 focus: base,
6658 disabled: Color::new(base.r, base.g, base.b, base.a * 0.4),
6659 focus_ring: Color::new(base.r, base.g, base.b, base.a * 0.7),
6660 }
6661 }
6662
6663 pub fn color_for(&self, state: InteractiveState) -> Color {
6665 match state {
6666 InteractiveState::Default => self.default,
6667 InteractiveState::Hover => self.hover,
6668 InteractiveState::Active => self.active,
6669 InteractiveState::Focus => self.focus,
6670 InteractiveState::Disabled => self.disabled,
6671 }
6672 }
6673 }
6674
6675 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6677 pub enum InteractiveState {
6678 Default,
6679 Hover,
6680 Active,
6681 Focus,
6682 Disabled,
6683 }
6684}
6685
6686pub fn use_state<T: Clone + Send + Sync + 'static>(
6705 id: u64,
6706 initial: T,
6707) -> (impl Fn() -> T, impl Fn(T)) {
6708 let already_exists = load_system_state().get_component_state::<T>(id).is_some();
6710 if !already_exists {
6711 update_system_state(|s| {
6712 let mut ns = s.clone();
6713 ns.set_component_state(id, initial.clone());
6714 ns
6715 });
6716 }
6717
6718 let getter = move || -> T {
6719 load_system_state()
6720 .get_component_state::<T>(id)
6721 .map(|arc_lock| {
6722 arc_lock
6723 .read()
6724 .ok()
6725 .map(|guard| (*guard).clone())
6726 .unwrap_or_else(|| initial.clone())
6727 })
6728 .unwrap_or_else(|| initial.clone())
6729 };
6730
6731 let setter = {
6732 move |value| {
6733 update_system_state(|s| {
6734 let mut ns = s.clone();
6735 ns.set_component_state(id, value);
6736 ns
6737 });
6738 }
6739 };
6740
6741 (getter, setter)
6742}
6743
6744pub fn use_state_hash(key: &str) -> u64 {
6756 use std::hash::{Hash, Hasher};
6757 let mut s = std::collections::hash_map::DefaultHasher::new();
6758 key.hash(&mut s);
6759 s.finish()
6760}
6761
6762thread_local! {
6772 static ACCESSIBILITY_PREFS: std::cell::RefCell<AccessibilityPreferences> =
6775 std::cell::RefCell::new(AccessibilityPreferences::default());
6776}
6777
6778#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6785pub struct AccessibilityPreferences {
6786 pub reduce_motion: bool,
6788 pub reduce_transparency: bool,
6790 pub increase_contrast: bool,
6792}
6793
6794impl AccessibilityPreferences {
6795 pub fn detect_from_system() -> Self {
6800 #[cfg(target_os = "macos")]
6801 {
6802 let reduce_motion = std::process::Command::new("defaults")
6804 .args(["read", "-g", "com.apple.universalaccess", "reduceMotion"])
6805 .output()
6806 .ok()
6807 .and_then(|o| String::from_utf8(o.stdout).ok())
6808 .map(|s| s.trim() == "1")
6809 .unwrap_or(false);
6810
6811 let reduce_transparency = std::process::Command::new("defaults")
6812 .args([
6813 "read",
6814 "-g",
6815 "com.apple.universalaccess",
6816 "reduceTransparency",
6817 ])
6818 .output()
6819 .ok()
6820 .and_then(|o| String::from_utf8(o.stdout).ok())
6821 .map(|s| s.trim() == "1")
6822 .unwrap_or(false);
6823
6824 let increase_contrast = std::process::Command::new("defaults")
6825 .args([
6826 "read",
6827 "-g",
6828 "com.apple.universalaccess",
6829 "increaseContrast",
6830 ])
6831 .output()
6832 .ok()
6833 .and_then(|o| String::from_utf8(o.stdout).ok())
6834 .map(|s| s.trim() == "1")
6835 .unwrap_or(false);
6836
6837 Self {
6838 reduce_motion,
6839 reduce_transparency,
6840 increase_contrast,
6841 }
6842 }
6843
6844 #[cfg(target_os = "linux")]
6845 {
6846 let reduce_motion = std::env::var("GTK_A11Y")
6848 .map(|v| v.to_lowercase().contains("reduce-motion"))
6849 .unwrap_or(false)
6850 || {
6851 std::process::Command::new("gsettings")
6853 .args([
6854 "get",
6855 "org.gnome.desktop.interface",
6856 "enable-animations",
6857 ])
6858 .output()
6859 .ok()
6860 .and_then(|o| String::from_utf8(o.stdout).ok())
6861 .map(|s| s.trim() == "'false'" || s.trim() == "false")
6862 .unwrap_or(false)
6863 };
6864
6865 let reduce_transparency = false;
6867
6868 let increase_contrast = std::env::var("GTK_THEME")
6870 .map(|v| v.to_lowercase().contains("highcontrast"))
6871 .unwrap_or(false);
6872
6873 Self {
6874 reduce_motion,
6875 reduce_transparency,
6876 increase_contrast,
6877 }
6878 }
6879
6880 #[cfg(target_os = "windows")]
6881 {
6882 use std::process::Command;
6883
6884 fn reg_query(key: &str, value_name: &str) -> Option<String> {
6886 Command::new("reg")
6887 .args(["query", key, "/v", value_name])
6888 .output()
6889 .ok()
6890 .and_then(|o| {
6891 if o.status.success() {
6892 String::from_utf8(o.stdout).ok()
6893 } else {
6894 None
6895 }
6896 })
6897 .and_then(|s| {
6898 s.lines()
6901 .last()?
6902 .split_whitespace()
6903 .last()
6904 .map(String::from)
6905 })
6906 }
6907
6908 let reduce_motion = reg_query(
6910 "HKCU\\Control Panel\\Accessibility\\EffectsAnimationEfficiency",
6911 "EffectsAnimationEfficiency",
6912 )
6913 .map(|v| v == "1")
6914 .unwrap_or(false);
6915
6916 let reduce_transparency = reg_query(
6918 "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
6919 "EnableTransparency",
6920 )
6921 .map(|v| v == "0")
6922 .unwrap_or(false);
6923
6924 let increase_contrast = reg_query(
6926 "HKCU\\Control Panel\\Accessibility\\HighContrast",
6927 "HighContrast",
6928 )
6929 .map(|v| v == "1")
6930 .unwrap_or(false);
6931
6932 Self {
6933 reduce_motion,
6934 reduce_transparency,
6935 increase_contrast,
6936 }
6937 }
6938
6939 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
6940 {
6941 Self::default()
6942 }
6943 }
6944
6945 pub fn min_alpha(&self, requested: f32) -> f32 {
6947 if self.increase_contrast {
6948 requested.max(0.5)
6949 } else {
6950 requested
6951 }
6952 }
6953
6954 pub fn should_disable_glass(&self) -> bool {
6956 self.reduce_transparency
6957 }
6958
6959 pub fn should_reduce_motion(&self) -> bool {
6961 self.reduce_motion
6962 }
6963
6964 pub fn should_increase_contrast(&self) -> bool {
6966 self.increase_contrast
6967 }
6968}
6969
6970pub fn accessibility_preferences() -> AccessibilityPreferences {
6972 ACCESSIBILITY_PREFS.with(|p| *p.borrow())
6973}
6974
6975pub fn set_accessibility_preferences(prefs: AccessibilityPreferences) {
6980 ACCESSIBILITY_PREFS.with(|p| {
6981 *p.borrow_mut() = prefs;
6982 });
6983}
6984
6985pub trait ClipboardProvider: Send + Sync {
6994 fn read_text(&self) -> Option<String>;
6996 fn write_text(&self, text: &str);
6998}
6999
7000#[cfg(all(not(target_arch = "wasm32"), target_os = "macos"))]
7004pub struct SystemClipboard;
7005
7006#[cfg(all(not(target_arch = "wasm32"), target_os = "macos"))]
7007impl ClipboardProvider for SystemClipboard {
7008 fn read_text(&self) -> Option<String> {
7009 use std::process::Command;
7010 Command::new("pbpaste")
7012 .output()
7013 .ok()
7014 .and_then(|o| String::from_utf8(o.stdout).ok())
7015 }
7016
7017 fn write_text(&self, text: &str) {
7018 use std::process::Command;
7019 if let Ok(mut child) = Command::new("pbcopy")
7021 .stdin(std::process::Stdio::piped())
7022 .spawn()
7023 {
7024 if let Some(stdin) = child.stdin.as_mut() {
7025 use std::io::Write;
7026 let _ = stdin.write_all(text.as_bytes());
7027 }
7028 let _ = child.wait();
7029 }
7030 }
7031}
7032
7033#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7039pub enum TextDirection {
7040 Forward,
7041 Backward,
7042 Up,
7043 Down,
7044 LineStart,
7045 LineEnd,
7046 WordForward,
7047 WordBackward,
7048}
7049
7050#[derive(Debug, Clone, Default)]
7055pub struct TextInputState {
7056 pub text: String,
7058 pub cursor_pos: usize,
7060 pub selection_anchor: Option<usize>,
7063 pub focused: bool,
7065 pub caret_visible: bool,
7067 pub last_edit_time: f32,
7069}
7070
7071impl TextInputState {
7072 pub fn new(text: impl Into<String>) -> Self {
7074 let text = text.into();
7075 let cursor_pos = text.len();
7076 Self {
7077 text,
7078 cursor_pos,
7079 selection_anchor: None,
7080 focused: false,
7081 caret_visible: true,
7082 last_edit_time: 0.0,
7083 }
7084 }
7085
7086 pub fn selection_range(&self) -> Option<(usize, usize)> {
7089 self.selection_anchor.map(|anchor| {
7090 if anchor <= self.cursor_pos {
7091 (anchor, self.cursor_pos)
7092 } else {
7093 (self.cursor_pos, anchor)
7094 }
7095 })
7096 }
7097
7098 pub fn selected_text(&self) -> String {
7100 self.selection_range()
7101 .map(|(start, end)| self.text[start..end].to_string())
7102 .unwrap_or_default()
7103 }
7104
7105 pub fn insert(&mut self, new_text: &str) {
7107 if let Some((start, end)) = self.selection_range() {
7108 self.text.replace_range(start..end, new_text);
7109 self.cursor_pos = start + new_text.len();
7110 } else {
7111 self.text.insert_str(self.cursor_pos, new_text);
7112 self.cursor_pos += new_text.len();
7113 }
7114 self.selection_anchor = None;
7115 }
7116
7117 pub fn delete(&mut self, backward: bool, count: usize) -> String {
7120 if let Some((start, end)) = self.selection_range() {
7121 let deleted = self.text[start..end].to_string();
7122 self.text.replace_range(start..end, "");
7123 self.cursor_pos = start;
7124 self.selection_anchor = None;
7125 return deleted;
7126 }
7127
7128 if backward && self.cursor_pos > 0 {
7129 let start = self.cursor_pos.saturating_sub(count);
7130 let deleted = self.text[start..self.cursor_pos].to_string();
7131 self.text.replace_range(start..self.cursor_pos, "");
7132 self.cursor_pos = start;
7133 deleted
7134 } else if !backward && self.cursor_pos < self.text.len() {
7135 let end = (self.cursor_pos + count).min(self.text.len());
7136 let deleted = self.text[self.cursor_pos..end].to_string();
7137 self.text.replace_range(self.cursor_pos..end, "");
7138 deleted
7139 } else {
7140 String::new()
7141 }
7142 }
7143
7144 pub fn move_cursor(&mut self, direction: TextDirection, extend_selection: bool) {
7146 if !extend_selection {
7147 self.selection_anchor = None;
7148 } else if self.selection_anchor.is_none() {
7149 self.selection_anchor = Some(self.cursor_pos);
7150 }
7151
7152 match direction {
7153 TextDirection::Forward if self.cursor_pos < self.text.len() => {
7154 let next = self.text[self.cursor_pos..]
7156 .char_indices()
7157 .nth(1)
7158 .map(|(i, _)| self.cursor_pos + i)
7159 .unwrap_or(self.text.len());
7160 self.cursor_pos = next;
7161 }
7162 TextDirection::Backward if self.cursor_pos > 0 => {
7163 let prev = self.text[..self.cursor_pos]
7164 .char_indices()
7165 .next_back()
7166 .map(|(i, _)| i)
7167 .unwrap_or(0);
7168 self.cursor_pos = prev;
7169 }
7170 TextDirection::LineStart => {
7171 self.cursor_pos = 0;
7172 }
7173 TextDirection::LineEnd => {
7174 self.cursor_pos = self.text.len();
7175 }
7176 TextDirection::WordForward => {
7177 let rest = &self.text[self.cursor_pos..];
7179 let after_word = rest
7181 .char_indices()
7182 .find(|(_, c)| !c.is_alphanumeric())
7183 .map(|(i, _)| i)
7184 .unwrap_or(rest.len());
7185 let after_space = rest[after_word..]
7187 .char_indices()
7188 .find(|(_, c)| !c.is_whitespace())
7189 .map(|(i, _)| after_word + i)
7190 .unwrap_or(rest.len());
7191 self.cursor_pos = (self.cursor_pos + after_space).min(self.text.len());
7192 }
7193 TextDirection::WordBackward => {
7194 let before = &self.text[..self.cursor_pos];
7195 let before_word = before
7197 .char_indices()
7198 .rev()
7199 .find(|(_, c)| !c.is_whitespace())
7200 .map(|(i, _)| i)
7201 .unwrap_or(0);
7202 let word_start = before[..before_word]
7204 .char_indices()
7205 .rev()
7206 .find(|(_, c)| !c.is_alphanumeric())
7207 .map(|(i, _)| i)
7208 .unwrap_or(0);
7209 self.cursor_pos = word_start;
7210 }
7211 _ => {} }
7213
7214 if !extend_selection {
7215 self.selection_anchor = None;
7216 }
7217 }
7218
7219 pub fn select_all(&mut self) {
7221 self.cursor_pos = self.text.len();
7222 self.selection_anchor = Some(0);
7223 }
7224
7225 pub fn cursor_byte_pos(&self) -> usize {
7227 self.cursor_pos
7228 }
7229}
7230
7231#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7233pub struct NotificationAction {
7234 pub id: String,
7236 pub title: String,
7238 pub is_destructive: bool,
7240}
7241
7242#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7244pub enum NotificationPriority {
7245 Passive,
7247 #[default]
7249 Active,
7250 TimeSensitive,
7252}
7253
7254#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
7256pub struct Notification {
7257 pub id: String,
7259 pub app_name: Option<String>,
7261 pub title: String,
7263 pub body: String,
7265 pub icon: Option<String>,
7267 pub sound: Option<String>,
7269 pub actions: Vec<NotificationAction>,
7271 pub timeout: Option<f32>,
7273 pub priority: NotificationPriority,
7275 pub timestamp: f32,
7277 pub dismissed: bool,
7279}
7280
7281#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, thiserror::Error)]
7283pub enum NotificationError {
7284 #[error("Notification permission denied")]
7286 PermissionDenied,
7287 #[error("Failed to post notification")]
7289 PostFailed,
7290}
7291
7292#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7294pub enum NotificationPermission {
7295 Granted,
7297 Denied,
7299 #[default]
7301 NotDetermined,
7302}
7303
7304pub trait NotificationHandler: Send + Sync {
7306 fn show(&self, notification: Notification) -> Result<(), NotificationError>;
7308 fn dismiss(&self, id: &str) -> Result<(), NotificationError>;
7310 fn request_permission(&self) -> NotificationPermission;
7312}
7313
7314static NEXT_NOTIFICATION_ID: std::sync::atomic::AtomicUsize =
7315 std::sync::atomic::AtomicUsize::new(1);
7316
7317#[derive(Clone, Copy, Debug, Default)]
7319pub struct DefaultNotificationHandler;
7320
7321impl NotificationHandler for DefaultNotificationHandler {
7322 fn show(&self, notification: Notification) -> Result<(), NotificationError> {
7324 let mut notif = notification;
7325 if notif.id.is_empty() {
7326 let id = NEXT_NOTIFICATION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7327 notif.id = format!("notif_{}", id);
7328 }
7329 update_system_state(|state| {
7330 let mut new_state = state.clone();
7331 new_state.notifications.push(notif.clone());
7332 new_state
7333 });
7334 Ok(())
7335 }
7336
7337 fn dismiss(&self, id: &str) -> Result<(), NotificationError> {
7339 update_system_state(|state| {
7340 let mut new_state = state.clone();
7341 for notif in &mut new_state.notifications {
7342 if notif.id == id {
7343 notif.dismissed = true;
7344 }
7345 }
7346 new_state
7347 });
7348 Ok(())
7349 }
7350
7351 fn request_permission(&self) -> NotificationPermission {
7353 NotificationPermission::Granted
7354 }
7355}
7356
7357static NOTIFICATION_HANDLER: once_cell::sync::OnceCell<std::sync::Arc<dyn NotificationHandler>> =
7358 once_cell::sync::OnceCell::new();
7359
7360pub fn set_notification_handler(handler: std::sync::Arc<dyn NotificationHandler>) {
7362 let _ = NOTIFICATION_HANDLER.set(handler);
7363}
7364
7365pub fn get_notification_handler() -> std::sync::Arc<dyn NotificationHandler> {
7367 NOTIFICATION_HANDLER
7368 .get_or_init(|| std::sync::Arc::new(DefaultNotificationHandler))
7369 .clone()
7370}
7371
7372#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7374pub struct FileFilter {
7375 pub name: String,
7377 pub extensions: Vec<String>,
7379}
7380
7381#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
7383pub enum FileDialogMode {
7384 #[default]
7386 OpenFile,
7387 OpenDirectory,
7389 SaveFile,
7391}
7392
7393#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7395pub struct FileDialog {
7396 pub title: String,
7398 pub default_path: Option<String>,
7400 pub filters: Vec<FileFilter>,
7402 pub mode: FileDialogMode,
7404 pub allow_multiple: bool,
7406}
7407
7408#[derive(Debug, thiserror::Error)]
7410pub enum FileDialogError {
7411 #[error("File dialog cancelled")]
7413 Cancelled,
7414 #[error("I/O error: {0}")]
7416 Io(#[from] std::io::Error),
7417 #[error("Platform error: {0}")]
7419 Platform(String),
7420}
7421
7422impl FileDialog {
7423 pub fn new(mode: FileDialogMode) -> Self {
7425 Self {
7426 mode,
7427 ..Default::default()
7428 }
7429 }
7430
7431 pub fn title(mut self, title: impl Into<String>) -> Self {
7433 self.title = title.into();
7434 self
7435 }
7436
7437 pub fn add_filter(mut self, name: &str, extensions: &[&str]) -> Self {
7439 self.filters.push(FileFilter {
7440 name: name.to_string(),
7441 extensions: extensions.iter().map(|s| s.to_string()).collect(),
7442 });
7443 self
7444 }
7445
7446 pub fn default_path(mut self, path: impl Into<String>) -> Self {
7448 self.default_path = Some(path.into());
7449 self
7450 }
7451
7452 pub fn allow_multiple(mut self, allow: bool) -> Self {
7454 self.allow_multiple = allow;
7455 self
7456 }
7457}
7458
7459#[cfg(not(target_arch = "wasm32"))]
7460impl FileDialog {
7461 pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7463 let mut dialog = rfd::FileDialog::new();
7464 dialog = dialog.set_title(&self.title);
7465 if let Some(path) = &self.default_path {
7466 dialog = dialog.set_directory(path);
7467 }
7468 for filter in &self.filters {
7469 let refs: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
7470 dialog = dialog.add_filter(&filter.name, &refs);
7471 }
7472
7473 match self.mode {
7474 FileDialogMode::OpenFile => {
7475 if self.allow_multiple {
7476 dialog.pick_files().ok_or(FileDialogError::Cancelled)
7477 } else {
7478 Ok(dialog.pick_file().into_iter().collect())
7479 }
7480 }
7481 FileDialogMode::OpenDirectory => Ok(dialog.pick_folder().into_iter().collect()),
7482 FileDialogMode::SaveFile => Ok(dialog.save_file().into_iter().collect()),
7483 }
7484 }
7485
7486 pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7488 let results = self.pick()?;
7489 Ok(results.into_iter().next())
7490 }
7491}
7492
7493#[cfg(target_arch = "wasm32")]
7494impl FileDialog {
7495 pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7497 Err(FileDialogError::Platform(
7498 "FileDialog is not supported synchronously on WebAssembly".to_string(),
7499 ))
7500 }
7501
7502 pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7504 Err(FileDialogError::Platform(
7505 "FileDialog is not supported synchronously on WebAssembly".to_string(),
7506 ))
7507 }
7508}
7509
7510#[derive(Debug, thiserror::Error)]
7512pub enum DocumentError {
7513 #[error("I/O error: {0}")]
7515 Io(#[from] std::io::Error),
7516 #[error("Parse error: {0}")]
7518 Parse(String),
7519 #[error("Serialization error: {0}")]
7521 Serialize(String),
7522}
7523
7524pub trait Document: Send + Sync {
7526 fn read_from(path: &std::path::Path) -> Result<Self, DocumentError>
7528 where
7529 Self: Sized;
7530
7531 fn write_to(&self, path: &std::path::Path) -> Result<(), DocumentError>;
7533
7534 fn is_dirty(&self) -> bool;
7536
7537 fn mark_clean(&mut self);
7539}
7540
7541pub struct AutoSaveManager {
7543 pub interval: f32,
7545 pub timer: f32,
7547 pub documents: Vec<(std::path::PathBuf, Box<dyn Document>)>,
7549}
7550
7551impl AutoSaveManager {
7552 pub fn new(interval: f32) -> Self {
7554 Self {
7555 interval,
7556 timer: 0.0,
7557 documents: Vec::new(),
7558 }
7559 }
7560
7561 pub fn register(&mut self, path: std::path::PathBuf, doc: Box<dyn Document>) {
7563 self.documents.push((path, doc));
7564 }
7565
7566 pub fn tick(&mut self, dt: f32) {
7568 self.timer += dt;
7569 if self.timer >= self.interval {
7570 self.timer = 0.0;
7571 for (path, doc) in &mut self.documents {
7572 if doc.is_dirty() {
7573 match doc.write_to(path) {
7574 Ok(()) => {
7575 doc.mark_clean();
7576 log::info!("[AutoSaveManager] Auto-saved document to {:?}", path);
7577 }
7578 Err(e) => {
7579 log::error!(
7580 "[AutoSaveManager] Failed to auto-save document to {:?}: {:?}",
7581 path,
7582 e
7583 );
7584 }
7585 }
7586 }
7587 }
7588 }
7589 }
7590}
7591
7592#[derive(Debug, Clone, PartialEq, Eq, Default)]
7600pub struct Modifiers {
7601 pub cmd: bool,
7603 pub shift: bool,
7605 pub alt: bool,
7607 pub ctrl: bool,
7609}
7610
7611#[derive(Debug, Clone)]
7613pub struct KeyboardShortcut {
7614 pub key: String,
7616 pub modifiers: Modifiers,
7618}
7619
7620impl KeyboardShortcut {
7621 pub fn cmd(key: impl Into<String>) -> Self {
7623 Self {
7624 key: key.into(),
7625 modifiers: Modifiers {
7626 cmd: true,
7627 ..Default::default()
7628 },
7629 }
7630 }
7631
7632 pub fn cmd_shift(key: impl Into<String>) -> Self {
7634 Self {
7635 key: key.into(),
7636 modifiers: Modifiers {
7637 cmd: true,
7638 shift: true,
7639 ..Default::default()
7640 },
7641 }
7642 }
7643}
7644
7645pub enum MenuItem {
7651 Action {
7653 label: String,
7654 shortcut: Option<KeyboardShortcut>,
7655 action: std::sync::Arc<dyn Fn() + Send + Sync>,
7656 enabled: bool,
7657 },
7658 Submenu { label: String, items: Vec<MenuItem> },
7660 Separator,
7662}
7663
7664impl std::fmt::Debug for MenuItem {
7665 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7666 match self {
7667 Self::Action { label, enabled, .. } => f
7668 .debug_struct("Action")
7669 .field("label", label)
7670 .field("enabled", enabled)
7671 .finish(),
7672 Self::Submenu { label, items } => f
7673 .debug_struct("Submenu")
7674 .field("label", label)
7675 .field("items", items)
7676 .finish(),
7677 Self::Separator => write!(f, "Separator"),
7678 }
7679 }
7680}
7681
7682pub struct MenuBar {
7687 pub items: Vec<MenuItem>,
7689}
7690
7691impl MenuBar {
7692 pub fn new() -> Self {
7694 Self { items: Vec::new() }
7695 }
7696
7697 pub fn add_item(&mut self, item: MenuItem) {
7699 self.items.push(item);
7700 }
7701
7702 #[allow(clippy::too_many_arguments)]
7714 pub fn standard(
7715 new_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7716 open_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7717 save_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7718 close_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7719 quit_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7720 undo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7721 redo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7722 cut_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7723 copy_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7724 paste_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7725 select_all_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7726 find_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7727 ) -> Self {
7728 let mut bar = Self::new();
7729
7730 bar.add_item(MenuItem::Submenu {
7732 label: "File".to_string(),
7733 items: vec![
7734 MenuItem::Action {
7735 label: "New".to_string(),
7736 shortcut: Some(KeyboardShortcut::cmd("n")),
7737 action: new_fn,
7738 enabled: true,
7739 },
7740 MenuItem::Action {
7741 label: "Open…".to_string(),
7742 shortcut: Some(KeyboardShortcut::cmd("o")),
7743 action: open_fn,
7744 enabled: true,
7745 },
7746 MenuItem::Separator,
7747 MenuItem::Action {
7748 label: "Save".to_string(),
7749 shortcut: Some(KeyboardShortcut::cmd("s")),
7750 action: save_fn,
7751 enabled: true,
7752 },
7753 MenuItem::Separator,
7754 MenuItem::Action {
7755 label: "Close".to_string(),
7756 shortcut: Some(KeyboardShortcut::cmd("w")),
7757 action: close_fn,
7758 enabled: true,
7759 },
7760 MenuItem::Separator,
7761 MenuItem::Action {
7762 label: "Quit".to_string(),
7763 shortcut: Some(KeyboardShortcut::cmd("q")),
7764 action: quit_fn,
7765 enabled: true,
7766 },
7767 ],
7768 });
7769
7770 bar.add_item(MenuItem::Submenu {
7772 label: "Edit".to_string(),
7773 items: vec![
7774 MenuItem::Action {
7775 label: "Undo".to_string(),
7776 shortcut: Some(KeyboardShortcut::cmd("z")),
7777 action: undo_fn,
7778 enabled: true,
7779 },
7780 MenuItem::Action {
7781 label: "Redo".to_string(),
7782 shortcut: Some(KeyboardShortcut::cmd_shift("z")),
7783 action: redo_fn,
7784 enabled: true,
7785 },
7786 MenuItem::Separator,
7787 MenuItem::Action {
7788 label: "Cut".to_string(),
7789 shortcut: Some(KeyboardShortcut::cmd("x")),
7790 action: cut_fn,
7791 enabled: true,
7792 },
7793 MenuItem::Action {
7794 label: "Copy".to_string(),
7795 shortcut: Some(KeyboardShortcut::cmd("c")),
7796 action: copy_fn,
7797 enabled: true,
7798 },
7799 MenuItem::Action {
7800 label: "Paste".to_string(),
7801 shortcut: Some(KeyboardShortcut::cmd("v")),
7802 action: paste_fn,
7803 enabled: true,
7804 },
7805 MenuItem::Separator,
7806 MenuItem::Action {
7807 label: "Select All".to_string(),
7808 shortcut: Some(KeyboardShortcut::cmd("a")),
7809 action: select_all_fn,
7810 enabled: true,
7811 },
7812 MenuItem::Separator,
7813 MenuItem::Action {
7814 label: "Find…".to_string(),
7815 shortcut: Some(KeyboardShortcut::cmd("f")),
7816 action: find_fn,
7817 enabled: true,
7818 },
7819 ],
7820 });
7821
7822 let noop: std::sync::Arc<dyn Fn() + Send + Sync> = std::sync::Arc::new(|| {});
7826 bar.add_item(MenuItem::Submenu {
7827 label: "View".to_string(),
7828 items: vec![
7829 MenuItem::Action {
7830 label: "Zoom In".to_string(),
7831 shortcut: Some(KeyboardShortcut::cmd("=")),
7832 action: noop.clone(),
7833 enabled: true,
7834 },
7835 MenuItem::Action {
7836 label: "Zoom Out".to_string(),
7837 shortcut: Some(KeyboardShortcut::cmd("-")),
7838 action: noop.clone(),
7839 enabled: true,
7840 },
7841 MenuItem::Separator,
7842 MenuItem::Action {
7843 label: "Toggle Fullscreen".to_string(),
7844 shortcut: Some(KeyboardShortcut {
7845 key: "f".to_string(),
7846 modifiers: Modifiers {
7847 ctrl: true,
7848 ..Default::default()
7849 },
7850 }),
7851 action: noop.clone(),
7852 enabled: true,
7853 },
7854 ],
7855 });
7856
7857 bar.add_item(MenuItem::Submenu {
7859 label: "Window".to_string(),
7860 items: vec![
7861 MenuItem::Action {
7862 label: "Minimize".to_string(),
7863 shortcut: Some(KeyboardShortcut::cmd("m")),
7864 action: noop.clone(),
7865 enabled: true,
7866 },
7867 MenuItem::Action {
7868 label: "Zoom".to_string(),
7869 shortcut: None,
7870 action: noop.clone(),
7871 enabled: true,
7872 },
7873 MenuItem::Separator,
7874 MenuItem::Action {
7875 label: "Bring All to Front".to_string(),
7876 shortcut: None,
7877 action: noop.clone(),
7878 enabled: true,
7879 },
7880 ],
7881 });
7882
7883 bar.add_item(MenuItem::Submenu {
7885 label: "Help".to_string(),
7886 items: vec![MenuItem::Action {
7887 label: "Search Help".to_string(),
7888 shortcut: None,
7889 action: noop,
7890 enabled: true,
7891 }],
7892 });
7893
7894 bar
7895 }
7896}
7897
7898impl Default for MenuBar {
7899 fn default() -> Self {
7900 Self::new()
7901 }
7902}
7903
7904use std::sync::RwLock;
7910
7911#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7913pub enum Direction {
7914 #[default]
7915 LTR,
7916 RTL,
7917 Auto,
7918}
7919
7920impl Direction {
7921 pub fn is_rtl(self) -> bool {
7922 matches!(self, Direction::RTL)
7923 }
7924}
7925#[derive(Clone, Debug)]
7926pub struct L10nBundle {
7927 pub locale: String,
7928 pub strings: HashMap<String, String>,
7929 pub is_rtl: bool,
7930}
7931
7932impl L10nBundle {
7933 pub fn new(locale: impl Into<String>) -> Self {
7934 Self {
7935 locale: locale.into(),
7936 strings: HashMap::new(),
7937 is_rtl: false,
7938 }
7939 }
7940
7941 pub fn add(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
7942 self.strings.insert(key.into(), value.into());
7943 self
7944 }
7945
7946 pub fn from_strings_format(locale: impl Into<String>, input: &str) -> Self {
7947 let mut bundle = Self::new(locale);
7948 for line in input.lines() {
7949 let line = line.trim();
7950 if line.is_empty() || line.starts_with("//") {
7951 continue;
7952 }
7953 if let Some(eq_pos) = line.find(" = ") {
7954 let key = line[..eq_pos].trim_matches('"').to_string();
7955 let val = line[eq_pos + 3..]
7956 .trim_end_matches(';')
7957 .trim_matches('"')
7958 .to_string();
7959 bundle.strings.insert(key, val);
7960 }
7961 }
7962 bundle
7963 }
7964 pub fn t(&self, key: &str) -> String {
7966 self.strings
7967 .get(key)
7968 .map(|s| s.to_string())
7969 .unwrap_or_else(|| key.to_string())
7970 }
7971
7972 pub fn tf(&self, key: &str, args: &[&str]) -> String {
7974 let mut result = self.t(key);
7975 for (i, arg) in args.iter().enumerate() {
7976 result = result.replace(&format!("{{{}}}", i), arg);
7977 }
7978 result
7979 }
7980}
7981
7982pub struct L10n {
7984 bundles: HashMap<String, L10nBundle>,
7985 current: String,
7986}
7987
7988impl L10n {
7989 pub fn new(default_locale: &str) -> Self {
7990 Self {
7991 bundles: HashMap::new(),
7992 current: default_locale.to_string(),
7993 }
7994 }
7995
7996 pub fn add_bundle(&mut self, bundle: L10nBundle) {
7997 self.bundles.insert(bundle.locale.clone(), bundle);
7998 }
7999
8000 pub fn set_locale(&mut self, locale: &str) {
8001 self.current = locale.to_string();
8002 }
8003 pub fn current_locale(&self) -> &str {
8004 &self.current
8005 }
8006
8007 pub fn is_rtl(&self) -> bool {
8008 self.bundles
8009 .get(self.current.as_str())
8010 .map(|b| b.is_rtl)
8011 .unwrap_or(false)
8012 }
8013
8014 pub fn t(&self, key: &str) -> String {
8015 self.bundles
8016 .get(self.current.as_str())
8017 .map(|b| b.t(key))
8018 .unwrap_or_else(|| key.to_string())
8019 }
8020
8021 pub fn tf(&self, key: &str, args: &[&str]) -> String {
8022 let mut result = self.t(key);
8023 for (i, arg) in args.iter().enumerate() {
8024 result = result.replace(&format!("{{{}}}", i), arg);
8025 }
8026 result
8027 }
8028
8029 pub fn direction(&self) -> Direction {
8030 if self.is_rtl() {
8031 Direction::RTL
8032 } else {
8033 Direction::LTR
8034 }
8035 }
8036}
8037
8038static L10N: once_cell::sync::Lazy<Arc<RwLock<L10n>>> =
8039 once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(L10n::new("en"))));
8040
8041pub fn init_l10n(l10n: L10n) {
8042 if let Ok(mut guard) = L10N.write() {
8043 *guard = l10n;
8044 }
8045}
8046
8047pub fn l10n() -> Arc<RwLock<L10n>> {
8048 L10N.clone()
8049}
8050
8051pub fn t(key: &str) -> String {
8052 L10N.read()
8053 .map(|g| g.t(key).to_string())
8054 .unwrap_or_else(|_| key.to_string())
8055}
8056
8057pub fn tf(key: &str, args: &[&str]) -> String {
8058 L10N.read()
8059 .map(|g| g.tf(key, args))
8060 .unwrap_or_else(|_| key.to_string())
8061}
8062
8063pub fn set_locale(locale: &str) {
8064 if let Ok(mut guard) = L10N.write() {
8065 guard.set_locale(locale);
8066 }
8067}
8068
8069pub fn current_locale() -> String {
8070 L10N.read()
8071 .map(|g| g.current_locale().to_string())
8072 .unwrap_or_else(|_| "en".to_string())
8073}
8074
8075pub fn is_rtl() -> bool {
8076 L10N.read().map(|g| g.is_rtl()).unwrap_or(false)
8077}
8078
8079#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
8091pub enum SystemTheme {
8092 #[default]
8094 Dark,
8095 Light,
8097}
8098
8099pub fn detect_system_theme() -> SystemTheme {
8109 std::env::var("CVKG_THEME")
8110 .ok()
8111 .and_then(|v| match v.as_str() {
8112 "light" => Some(SystemTheme::Light),
8113 "dark" => Some(SystemTheme::Dark),
8114 _ => None,
8115 })
8116 .unwrap_or(SystemTheme::Dark)
8117}
8118
8119pub mod audio_haptic;
8125pub use audio_haptic::{
8126 AudioEngine, HapticEngine, HapticIntensity, NullAudioEngine, NullHapticEngine, haptic_error,
8127 haptic_impact, haptic_selection, haptic_success, play_sound, set_audio_engine,
8128 set_haptic_engine, sounds,
8129};
8130
8131pub mod parallax;
8136pub use parallax::{DisplayEnvironment, ParallaxModifier, PerformanceContract, Tier3Fallback};
8137
8138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8157pub struct KvasirId(pub u64);
8158
8159impl KvasirId {
8160 pub const NULL: KvasirId = KvasirId(0);
8162
8163 pub fn new() -> Self {
8169 use std::sync::atomic::{AtomicU64, Ordering};
8170 static COUNTER: AtomicU64 = AtomicU64::new(1);
8171 KvasirId(COUNTER.fetch_add(1, Ordering::Relaxed))
8172 }
8173
8174 pub fn is_null(self) -> bool {
8176 self.0 == 0
8177 }
8178}
8179
8180impl std::fmt::Display for KvasirId {
8181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8182 write!(f, "KvasirId({})", self.0)
8183 }
8184}
8185
8186impl From<u64> for KvasirId {
8203 fn from(value: u64) -> Self {
8204 KvasirId(value)
8205 }
8206}
8207
8208impl From<KvasirId> for u64 {
8209 fn from(id: KvasirId) -> Self {
8210 id.0
8211 }
8212}
8213
8214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8237pub struct DirtyFlags(pub u8);
8238
8239impl DirtyFlags {
8240 pub const CLEAN: DirtyFlags = DirtyFlags(0b0000_0000);
8242 pub const STATE: DirtyFlags = DirtyFlags(0b0000_1111);
8244 pub const LAYOUT: DirtyFlags = DirtyFlags(0b0000_0111);
8246 pub const PAINT: DirtyFlags = DirtyFlags(0b0000_0011);
8248 pub const COMPOSITE: DirtyFlags = DirtyFlags(0b0000_0001);
8250 pub const ALL: DirtyFlags = DirtyFlags(0b0000_1111);
8252
8253 #[inline]
8255 pub fn is_dirty(self) -> bool {
8256 self.0 != 0
8257 }
8258
8259 #[inline]
8261 pub fn needs_composite(self) -> bool {
8262 self.0 & 0b0000_0001 != 0
8263 }
8264
8265 #[inline]
8267 pub fn needs_paint(self) -> bool {
8268 self.0 & 0b0000_0010 != 0
8269 }
8270
8271 #[inline]
8273 pub fn needs_layout(self) -> bool {
8274 self.0 & 0b0000_0100 != 0
8275 }
8276
8277 #[inline]
8279 pub fn needs_state(self) -> bool {
8280 self.0 & 0b0000_1000 != 0
8281 }
8282
8283 #[inline]
8285 pub fn merge(self, other: DirtyFlags) -> DirtyFlags {
8286 DirtyFlags(self.0 | other.0)
8287 }
8288
8289 #[inline]
8291 pub fn clear(self) -> DirtyFlags {
8292 DirtyFlags::CLEAN
8293 }
8294}
8295
8296impl std::ops::BitOr for DirtyFlags {
8297 type Output = DirtyFlags;
8298 fn bitor(self, rhs: DirtyFlags) -> DirtyFlags {
8299 DirtyFlags(self.0 | rhs.0)
8300 }
8301}
8302
8303impl std::ops::BitOrAssign for DirtyFlags {
8304 fn bitor_assign(&mut self, rhs: DirtyFlags) {
8305 self.0 |= rhs.0;
8306 }
8307}
8308
8309impl std::ops::BitAnd for DirtyFlags {
8310 type Output = DirtyFlags;
8311 fn bitand(self, rhs: DirtyFlags) -> DirtyFlags {
8312 DirtyFlags(self.0 & rhs.0)
8313 }
8314}
8315
8316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8322pub struct InvalidationRecord {
8323 pub id: KvasirId,
8325 pub flags: DirtyFlags,
8327}
8328
8329impl InvalidationRecord {
8330 pub fn new(id: KvasirId, flags: DirtyFlags) -> Self {
8332 Self { id, flags }
8333 }
8334
8335 pub fn full(id: KvasirId) -> Self {
8337 Self { id, flags: DirtyFlags::ALL }
8338 }
8339}
8340
8341#[cfg(test)]
8342mod kvasir_identity_tests {
8343 use super::*;
8344
8345 #[test]
8346 fn kvasir_id_new_is_non_zero() {
8347 let id = KvasirId::new();
8349 assert!(!id.is_null(), "KvasirId::new() returned null sentinel");
8350 }
8351
8352 #[test]
8353 fn kvasir_id_new_is_unique() {
8354 let a = KvasirId::new();
8356 let b = KvasirId::new();
8357 let c = KvasirId::new();
8358 assert_ne!(a, b);
8359 assert_ne!(b, c);
8360 assert_ne!(a, c);
8361 }
8362
8363 #[test]
8364 fn kvasir_id_null_sentinel() {
8365 assert!(KvasirId::NULL.is_null());
8366 assert!(!KvasirId::new().is_null());
8367 }
8368
8369 #[test]
8370 fn kvasir_id_serde_roundtrip() {
8371 let id = KvasirId(42);
8372 let json = serde_json::to_string(&id).unwrap();
8373 let decoded: KvasirId = serde_json::from_str(&json).unwrap();
8374 assert_eq!(id, decoded);
8375 }
8376
8377 #[test]
8378 fn dirty_flags_clean_is_not_dirty() {
8379 assert!(!DirtyFlags::CLEAN.is_dirty());
8380 }
8381
8382 #[test]
8383 fn dirty_flags_all_implies_all_layers() {
8384 let f = DirtyFlags::ALL;
8385 assert!(f.needs_state());
8386 assert!(f.needs_layout());
8387 assert!(f.needs_paint());
8388 assert!(f.needs_composite());
8389 }
8390
8391 #[test]
8392 fn dirty_flags_composite_only() {
8393 let f = DirtyFlags::COMPOSITE;
8394 assert!(!f.needs_state());
8395 assert!(!f.needs_layout());
8396 assert!(!f.needs_paint());
8397 assert!(f.needs_composite());
8398 }
8399
8400 #[test]
8401 fn dirty_flags_merge() {
8402 let a = DirtyFlags::COMPOSITE;
8403 let b = DirtyFlags::PAINT;
8404 let merged = a.merge(b);
8405 assert!(merged.needs_composite());
8406 assert!(merged.needs_paint());
8407 assert!(!merged.needs_layout());
8408 }
8409
8410 #[test]
8411 fn dirty_flags_bitor() {
8412 let combined = DirtyFlags::PAINT | DirtyFlags::COMPOSITE;
8413 assert!(combined.needs_paint());
8414 assert!(combined.needs_composite());
8415 }
8416
8417 #[test]
8418 fn dirty_flags_clear() {
8419 let dirty = DirtyFlags::ALL;
8420 let clean = dirty.clear();
8421 assert!(!clean.is_dirty());
8422 }
8423
8424 #[test]
8425 fn dirty_flags_serde_roundtrip() {
8426 let f = DirtyFlags::LAYOUT;
8427 let json = serde_json::to_string(&f).unwrap();
8428 let decoded: DirtyFlags = serde_json::from_str(&json).unwrap();
8429 assert_eq!(f, decoded);
8430 }
8431
8432 #[test]
8433 fn invalidation_record_full() {
8434 let id = KvasirId::new();
8435 let rec = InvalidationRecord::full(id);
8436 assert_eq!(rec.id, id);
8437 assert!(rec.flags.needs_state());
8438 assert!(rec.flags.needs_layout());
8439 }
8440}
8441
8442#[cfg(test)]
8452mod subscriber_panic_isolation_tests {
8453 use super::*;
8454 use std::sync::atomic::{AtomicUsize, Ordering};
8455
8456 #[test]
8457 fn panicking_subscriber_does_not_poison_mutex() {
8458 let state = State::new(0i32);
8459 let fired = Arc::new(AtomicUsize::new(0));
8460
8461 let _ = state.subscribe(|_| -> () {
8463 panic!("subscriber 1 explodes");
8464 });
8465
8466 let fired_clone = Arc::clone(&fired);
8468 let _ = state.subscribe(move |v| {
8469 fired_clone.store(*v as usize + 1, Ordering::SeqCst);
8470 });
8471
8472 state.set(42);
8474
8475 assert_eq!(
8476 fired.load(Ordering::SeqCst),
8477 43,
8478 "second subscriber must fire even though first panicked"
8479 );
8480
8481 let fired2 = Arc::new(AtomicUsize::new(0));
8483 let fired2_clone = Arc::clone(&fired2);
8484 let _ = state.subscribe(move |v| {
8485 fired2_clone.store(*v as usize, Ordering::SeqCst);
8486 });
8487 state.set(100);
8488 assert_eq!(
8489 fired2.load(Ordering::SeqCst),
8490 100,
8491 "future updates must work after subscriber panic"
8492 );
8493 }
8494
8495 #[test]
8496 fn all_subscribers_fire_even_if_one_panics() {
8497 let state = State::new(0u32);
8498 let count = Arc::new(AtomicUsize::new(0));
8499
8500 let _ = state.subscribe(|_| panic!("boom 1"));
8502 let c1 = Arc::clone(&count);
8503 let _ = state.subscribe(move |_| {
8504 c1.fetch_add(1, Ordering::SeqCst);
8505 });
8506 let _ = state.subscribe(|_| panic!("boom 2"));
8507 let c2 = Arc::clone(&count);
8508 let _ = state.subscribe(move |_| {
8509 c2.fetch_add(1, Ordering::SeqCst);
8510 });
8511
8512 state.set(1);
8513
8514 assert_eq!(
8516 count.load(Ordering::SeqCst),
8517 2,
8518 "both non-panicking subscribers should fire"
8519 );
8520 }
8521
8522 #[test]
8523 fn invoke_subscribers_safely_returns_count() {
8524 use std::sync::Mutex;
8526 let subs: SubscriberList<u32> = Arc::new(Mutex::new(Vec::new()));
8527
8528 let count1 = Arc::new(AtomicUsize::new(0));
8529 let count1_clone = Arc::clone(&count1);
8530 subs.lock().unwrap().push(Box::new(move |v| {
8531 count1_clone.store(*v as usize, Ordering::SeqCst);
8532 }));
8533
8534 let count2 = Arc::new(AtomicUsize::new(0));
8535 let count2_clone = Arc::clone(&count2);
8536 subs.lock().unwrap().push(Box::new(move |v| {
8537 count2_clone.store(*v as usize + 100, Ordering::SeqCst);
8538 }));
8539
8540 let invoked = invoke_subscribers_safely(&subs, &7);
8541 assert_eq!(invoked, 2, "both subscribers should be invoked");
8542 assert_eq!(count1.load(Ordering::SeqCst), 7);
8543 assert_eq!(count2.load(Ordering::SeqCst), 107);
8544 }
8545}
8546
8547#[cfg(test)]
8556mod p1_17_shared_fallback_runtime_tests {
8557 use super::*;
8558 use std::time::Duration;
8559
8560 #[test]
8561 fn fallback_runtime_is_shared() {
8562 let r1 = fallback_runtime();
8566 let r2 = fallback_runtime();
8567 assert!(
8568 std::ptr::eq(r1 as *const _, r2 as *const _),
8569 "fallback_runtime must return the same instance"
8570 );
8571 }
8572
8573 #[test]
8574 fn fallback_worker_count_is_bounded() {
8575 let n = *FALLBACK_WORKER_COUNT.get_or_init(|| {
8579 let available = std::thread::available_parallelism()
8580 .map(|n| n.get())
8581 .unwrap_or(2);
8582 available.saturating_sub(1).clamp(1, 8)
8583 });
8584 assert!(n >= 1, "worker count must be at least 1, got {n}");
8585 assert!(n <= 8, "worker count must be at most 8, got {n}");
8586 }
8587
8588 #[test]
8589 fn many_suspense_calls_share_runtime() {
8590 let counter = State::new(0u32);
8598 let mut handles = Vec::new();
8599 for _ in 0..20 {
8600 let s = Suspense::new_async(async { Ok::<u32, String>(1) });
8601 let _ = s; handles.push(s);
8607 }
8608 counter.set(20);
8610 assert_eq!(counter.get(), 20);
8611 }
8614
8615 #[test]
8620 fn p1_14_state_storage_mechanisms() {
8621 use std::mem::size_of;
8628 let state = State::new(42u32);
8629 let size = size_of_val(&state);
8633 assert!(
8637 size >= 4 * std::mem::size_of::<usize>(),
8638 "State<T> should be at least 4 Arcs in size"
8639 );
8640 }
8641
8642 #[test]
8643 fn p1_14_set_direct_updates_value() {
8644 let state = State::new(0u32);
8647 state.set_direct(42);
8648 assert_eq!(state.get(), 42);
8649 }
8650
8651 #[test]
8652 fn p1_14_set_direct_notifies_subscribers() {
8653 let state = State::new(0u32);
8656 let received = Arc::new(Mutex::new(Vec::new()));
8657 let received_clone = Arc::clone(&received);
8658 state.subscribe(move |v| {
8659 received_clone.lock().unwrap().push(*v);
8660 });
8661 state.set_direct(1);
8662 state.set_direct(2);
8663 state.set_direct(3);
8664 std::thread::sleep(std::time::Duration::from_millis(10));
8666 let log = received.lock().unwrap();
8667 assert!(
8671 log.contains(&1) && log.contains(&2) && log.contains(&3),
8672 "set_direct must notify subscribers of all values"
8673 );
8674 }
8675}
8676
8677#[derive(Debug, Clone, Default)]
8700pub struct DirtyRegionManager {
8701 regions: Vec<Rect>,
8703 generation: u64,
8706}
8707
8708impl DirtyRegionManager {
8709 pub fn new() -> Self {
8711 Self::default()
8712 }
8713
8714 pub fn mark_dirty(&mut self, region: Rect) {
8723 for existing in self.regions.iter_mut() {
8725 if Self::rects_overlap(*existing, region) {
8726 *existing = Self::union_rect(*existing, region);
8727 return;
8728 }
8729 }
8730 self.regions.push(region);
8732 }
8733
8734 pub fn regions(&self) -> &[Rect] {
8737 &self.regions
8738 }
8739
8740 pub fn is_dirty(&self) -> bool {
8743 !self.regions.is_empty()
8744 }
8745
8746 pub fn clear(&mut self) {
8752 self.regions.clear();
8753 self.generation = self.generation.wrapping_add(1);
8754 }
8755
8756 pub fn generation(&self) -> u64 {
8760 self.generation
8761 }
8762
8763 pub fn len(&self) -> usize {
8767 self.regions.len()
8768 }
8769
8770 pub fn is_empty(&self) -> bool {
8772 self.regions.is_empty()
8773 }
8774
8775 fn rects_overlap(a: Rect, b: Rect) -> bool {
8777 a.x < b.x + b.width
8778 && a.x + a.width > b.x
8779 && a.y < b.y + b.height
8780 && a.y + a.height > b.y
8781 }
8782
8783 fn union_rect(a: Rect, b: Rect) -> Rect {
8786 let min_x = a.x.min(b.x);
8787 let min_y = a.y.min(b.y);
8788 let max_x = (a.x + a.width).max(b.x + b.width);
8789 let max_y = (a.y + a.height).max(b.y + b.height);
8790 Rect {
8791 x: min_x,
8792 y: min_y,
8793 width: max_x - min_x,
8794 height: max_y - min_y,
8795 }
8796 }
8797}
8798
8799#[cfg(test)]
8800mod p1_39_dirty_region_tests {
8801 use super::{DirtyRegionManager, Rect};
8802
8803 #[test]
8804 fn new_manager_is_empty() {
8805 let m = DirtyRegionManager::new();
8806 assert!(!m.is_dirty());
8807 assert!(m.is_empty());
8808 assert_eq!(m.len(), 0);
8809 }
8810
8811 #[test]
8812 fn mark_dirty_adds_region() {
8813 let mut m = DirtyRegionManager::new();
8814 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8815 assert!(m.is_dirty());
8816 assert_eq!(m.len(), 1);
8817 }
8818
8819 #[test]
8820 fn overlapping_regions_coalesce() {
8821 let mut m = DirtyRegionManager::new();
8822 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8823 m.mark_dirty(Rect { x: 5.0, y: 5.0, width: 10.0, height: 10.0 });
8824 assert_eq!(m.len(), 1);
8826 let r = &m.regions()[0];
8827 assert_eq!(r.x, 0.0);
8828 assert_eq!(r.y, 0.0);
8829 assert_eq!(r.width, 15.0);
8830 assert_eq!(r.height, 15.0);
8831 }
8832
8833 #[test]
8834 fn non_overlapping_regions_dont_coalesce() {
8835 let mut m = DirtyRegionManager::new();
8836 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8837 m.mark_dirty(Rect { x: 100.0, y: 100.0, width: 10.0, height: 10.0 });
8838 assert_eq!(m.len(), 2);
8840 }
8841
8842 #[test]
8843 fn clear_resets_regions_and_increments_generation() {
8844 let mut m = DirtyRegionManager::new();
8845 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8846 let g1 = m.generation();
8847 m.clear();
8848 assert!(!m.is_dirty());
8849 assert_eq!(m.len(), 0);
8850 assert_eq!(m.generation(), g1 + 1);
8851 }
8852
8853 #[test]
8854 fn many_overlapping_marks_coalesce_to_one() {
8855 let mut m = DirtyRegionManager::new();
8856 for i in 0..100 {
8858 m.mark_dirty(Rect {
8859 x: i as f32,
8860 y: i as f32,
8861 width: 10.0,
8862 height: 10.0,
8863 });
8864 }
8865 assert_eq!(m.len(), 1);
8867 }
8868}
8869
8870#[derive(Debug, Clone, PartialEq)]
8901pub struct VirtualWindow {
8902 pub first_visible: usize,
8904 pub last_visible: usize,
8906 pub offset_before: f32,
8909 pub offset_after: f32,
8912}
8913
8914pub fn compute_virtual_list_window(
8927 total_rows: usize,
8928 row_height: f32,
8929 viewport_y: f32,
8930 viewport_height: f32,
8931 overscan: usize,
8932) -> VirtualWindow {
8933 if total_rows == 0 || row_height <= 0.0 {
8934 return VirtualWindow {
8935 first_visible: 0,
8936 last_visible: 0,
8937 offset_before: 0.0,
8938 offset_after: 0.0,
8939 };
8940 }
8941
8942 let visible_rows = (viewport_height / row_height).ceil() as usize;
8944
8945 let first = (viewport_y / row_height).floor() as isize - overscan as isize;
8947 let first = first.max(0) as usize;
8948
8949 let last = first + visible_rows + 2 * overscan;
8951 let last = last.min(total_rows);
8952
8953 VirtualWindow {
8954 first_visible: first,
8955 last_visible: last,
8956 offset_before: first as f32 * row_height,
8957 offset_after: (total_rows - last) as f32 * row_height,
8958 }
8959}
8960
8961pub fn compute_virtual_list_window_variable(
8974 prefix_heights: &[f32],
8975 viewport_y: f32,
8976 viewport_height: f32,
8977 overscan: usize,
8978) -> VirtualWindow {
8979 let total_rows = prefix_heights.len().saturating_sub(1);
8980 if total_rows == 0 {
8981 return VirtualWindow {
8982 first_visible: 0,
8983 last_visible: 0,
8984 offset_before: 0.0,
8985 offset_after: 0.0,
8986 };
8987 }
8988
8989 let first_idx = prefix_heights
8991 .partition_point(|&h| h < viewport_y)
8992 .saturating_sub(1);
8993 let first = first_idx.saturating_sub(overscan);
8994
8995 let viewport_bottom = viewport_y + viewport_height;
8997 let last_idx = prefix_heights.partition_point(|&h| h < viewport_bottom);
8998 let last = (last_idx + overscan).min(total_rows);
8999
9000 VirtualWindow {
9001 first_visible: first,
9002 last_visible: last,
9003 offset_before: prefix_heights[first],
9004 offset_after: prefix_heights[total_rows] - prefix_heights[last],
9005 }
9006}