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 error_boundary;
48pub mod knowledge;
49pub mod renderer;
50pub mod undo;
51pub mod window;
52
53pub use asset::{AssetKey, AssetState, TokenValue, YggdrasilTokens};
55pub use error_boundary::{ComponentErrorState, ErrorBoundary};
56pub use knowledge::{AnnouncementPriority, KnowledgeFragment, KnowledgeId, KnowledgeState, MemoryLayer, Realm, TemporalEdge, TemporalNode};
57pub use undo::{UndoGroup, UndoManager};
58pub use window::{Window, WindowCloseAction, WindowConfig, WindowHandle, WindowId, WindowLevel};
59
60pub trait View: Sized + Send {
61 type Body: View;
64
65 fn body(self) -> Self::Body;
66
67 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
70
71 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
74 Size::ZERO
75 }
76
77 fn layout(&self) -> Option<&dyn layout::LayoutView> {
79 None
80 }
81
82 fn flex_weight(&self) -> f32 {
84 0.0
85 }
86
87 fn get_grid_placement(&self) -> Option<GridPlacement> {
89 None
90 }
91
92 fn modifier<M: ViewModifier>(self, m: M) -> ModifiedView<Self, M> {
94 ModifiedView::new(self, m)
95 }
96
97 fn bifrost(
99 self,
100 blur: f32,
101 saturation: f32,
102 opacity: f32,
103 ) -> ModifiedView<Self, BifrostModifier> {
104 self.modifier(BifrostModifier {
105 blur,
106 saturation,
107 opacity,
108 fresnel_strength: 1.0,
109 })
110 }
111
112 fn bifrost_full(
114 self,
115 blur: f32,
116 saturation: f32,
117 opacity: f32,
118 fresnel_strength: f32,
119 ) -> ModifiedView<Self, BifrostModifier> {
120 self.modifier(BifrostModifier {
121 blur,
122 saturation,
123 opacity,
124 fresnel_strength,
125 })
126 }
127
128 fn gungnir(
130 self,
131 color: impl Into<String>,
132 radius: f32,
133 intensity: f32,
134 ) -> ModifiedView<Self, GungnirModifier> {
135 self.modifier(GungnirModifier {
136 color: color.into(),
137 radius,
138 intensity,
139 })
140 }
141
142 fn mjolnir_slice(self, angle: f32, offset: f32) -> ModifiedView<Self, MjolnirSliceModifier> {
144 self.modifier(MjolnirSliceModifier { angle, offset })
145 }
146
147 fn mjolnir_shatter(
149 self,
150 pieces: u32,
151 force: f32,
152 ) -> ModifiedView<Self, MjolnirShatterModifier> {
153 self.modifier(MjolnirShatterModifier { pieces, force })
154 }
155
156 fn bifrost_bridge(self, id: impl Into<String>) -> ModifiedView<Self, BifrostBridgeModifier> {
158 self.modifier(BifrostBridgeModifier { id: id.into() })
159 }
160
161 fn background(self, color: [f32; 4]) -> ModifiedView<Self, BackgroundModifier> {
163 self.modifier(BackgroundModifier { color })
164 }
165
166 fn padding(self, amount: f32) -> ModifiedView<Self, PaddingModifier> {
168 self.modifier(PaddingModifier { amount })
169 }
170
171 fn opacity(self, opacity: f32) -> ModifiedView<Self, OpacityModifier> {
173 self.modifier(OpacityModifier {
174 opacity: opacity.clamp(0.0, 1.0),
175 })
176 }
177
178 fn foreground_color(self, color: [f32; 4]) -> ModifiedView<Self, ForegroundColorModifier> {
180 self.modifier(ForegroundColorModifier { color })
181 }
182
183 fn frame(self, width: Option<f32>, height: Option<f32>) -> ModifiedView<Self, FrameModifier> {
186 self.modifier(FrameModifier {
187 width,
188 height,
189 min_width: None,
190 max_width: None,
191 min_height: None,
192 max_height: None,
193 alignment: Alignment::Center,
194 })
195 }
196
197 fn flex(self, weight: f32) -> ModifiedView<Self, FlexModifier> {
199 self.modifier(FlexModifier { weight })
200 }
201
202 fn grid_placement(self, placement: GridPlacement) -> ModifiedView<Self, GridPlacementModifier> {
204 self.modifier(GridPlacementModifier { placement })
205 }
206
207 fn overlay<O: View + Clone + 'static>(
209 self,
210 overlay: O,
211 alignment: Alignment,
212 offset: [f32; 2],
213 on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
214 ) -> ModifiedView<Self, OverlayModifier> {
215 self.modifier(OverlayModifier {
216 overlay: overlay.erase(),
217 alignment,
218 offset,
219 on_dismiss,
220 })
221 }
222
223 fn safe_area_padding(self) -> ModifiedView<Self, SafeAreaModifier> {
225 self.modifier(SafeAreaModifier { ignores: false })
226 }
227
228 fn ignores_safe_area(self) -> ModifiedView<Self, SafeAreaModifier> {
230 self.modifier(SafeAreaModifier { ignores: true })
231 }
232
233 fn clip_to_bounds(self) -> ModifiedView<Self, ClipModifier> {
235 self.modifier(ClipModifier)
236 }
237
238 fn border(self, color: [f32; 4], width: f32) -> ModifiedView<Self, BorderModifier> {
240 self.modifier(BorderModifier { color, width })
241 }
242
243 fn elevation(self, level: f32) -> ModifiedView<Self, ElevationModifier> {
245 self.modifier(ElevationModifier { level })
246 }
247
248 fn magnetic(self, radius: f32, intensity: f32) -> ModifiedView<Self, MagneticModifier> {
250 self.modifier(MagneticModifier { radius, intensity })
251 }
252
253 fn mani_glow(self, color: [f32; 4], radius: f32) -> ModifiedView<Self, ManiGlowModifier> {
255 self.modifier(ManiGlowModifier { color, radius })
256 }
257
258 fn memory_layer(self, layer: MemoryLayer) -> ModifiedView<Self, BifrostLayerModifier> {
260 self.modifier(BifrostLayerModifier { layer })
261 }
262
263 fn fafnir_evolve(self, id: u64) -> ModifiedView<Self, FafnirModifier> {
265 self.modifier(FafnirModifier { id })
266 }
267
268 fn mimir_intent(self) -> ModifiedView<Self, MimirIntentModifier> {
270 self.modifier(MimirIntentModifier)
271 }
272
273 fn kvasir_vibes(self, complexity: f32) -> ModifiedView<Self, KvasirVibeModifier> {
275 self.modifier(KvasirVibeModifier { complexity })
276 }
277
278 fn odins_eye(self) -> ModifiedView<Self, OdinsEyeModifier> {
280 self.modifier(OdinsEyeModifier)
281 }
282
283 fn on_appear<F: Fn() + Send + Sync + 'static>(
285 self,
286 action: F,
287 ) -> ModifiedView<Self, LifecycleModifier> {
288 self.modifier(LifecycleModifier {
289 on_appear: Some(Arc::new(action)),
290 on_disappear: None,
291 })
292 }
293
294 fn on_disappear<F: Fn() + Send + Sync + 'static>(
296 self,
297 action: F,
298 ) -> ModifiedView<Self, LifecycleModifier> {
299 self.modifier(LifecycleModifier {
300 on_appear: None,
301 on_disappear: Some(Arc::new(action)),
302 })
303 }
304
305 fn on_click<F: Fn() + Send + Sync + 'static>(
307 self,
308 action: F,
309 ) -> ModifiedView<Self, OnClickModifier> {
310 self.modifier(OnClickModifier {
311 action: Arc::new(action),
312 })
313 }
314
315 fn on_pointer_enter<F: Fn() + Send + Sync + 'static>(
317 self,
318 action: F,
319 ) -> ModifiedView<Self, OnPointerEnterModifier> {
320 self.modifier(OnPointerEnterModifier {
321 action: Arc::new(action),
322 })
323 }
324
325 fn on_pointer_leave<F: Fn() + Send + Sync + 'static>(
327 self,
328 action: F,
329 ) -> ModifiedView<Self, OnPointerLeaveModifier> {
330 self.modifier(OnPointerLeaveModifier {
331 action: Arc::new(action),
332 })
333 }
334
335 fn on_pointer_move<F: Fn(f32, f32) + Send + Sync + 'static>(
337 self,
338 action: F,
339 ) -> ModifiedView<Self, OnPointerMoveModifier> {
340 self.modifier(OnPointerMoveModifier {
341 action: Arc::new(action),
342 })
343 }
344
345 fn on_pointer_down<F: Fn() + Send + Sync + 'static>(
347 self,
348 action: F,
349 ) -> ModifiedView<Self, OnPointerDownModifier> {
350 self.modifier(OnPointerDownModifier {
351 action: Arc::new(action),
352 })
353 }
354
355 fn on_pointer_up<F: Fn() + Send + Sync + 'static>(
357 self,
358 action: F,
359 ) -> ModifiedView<Self, OnPointerUpModifier> {
360 self.modifier(OnPointerUpModifier {
361 action: Arc::new(action),
362 })
363 }
364
365 fn erase(self) -> AnyView
367 where
368 Self: Clone + 'static,
369 {
370 AnyView::new(self)
371 }
372
373 fn aria_properties(&self) -> Option<AriaProperties> {
381 None
382 }
383
384 fn on_key_event(&self, _key: &str, _modifiers: KeyModifiers) -> bool {
387 false
388 }
389
390 fn key_shortcuts(&self) -> Vec<KeyShortcut> {
392 vec![]
393 }
394
395 fn changed(&self) -> bool { true }
399
400 fn view_id(&self) -> Option<u64> { None }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
412pub enum AriaRole {
413 Alert,
414 Alertdialog,
415 Article,
416 Banner,
417 Button,
418 Checkbox,
419 Columnheader,
420 Combobox,
421 Complementary,
422 Contentinfo,
423 Dialog,
424 Form,
425 Grid,
426 Gridcell,
427 Heading,
428 Img,
429 Link,
430 List,
431 Listbox,
432 Listitem,
433 Main,
434 Menu,
435 Menubar,
436 Menuitem,
437 Menuitemcheckbox,
438 Menuitemradio,
439 Navigation,
440 None,
441 Note,
442 Option,
443 Presentation,
444 Progressbar,
445 Radio,
446 Radiogroup,
447 Region,
448 Row,
449 Rowgroup,
450 Rowheader,
451 Search,
452 Separator,
453 Slider,
454 Spinbutton,
455 Status,
456 Switch,
457 Tab,
458 Table,
459 Tablist,
460 Tabpanel,
461 Textbox,
462 Toolbar,
463 Tooltip,
464 Tree,
465 Treeitem,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct AriaProperties {
471 pub role: AriaRole,
472 pub label: String,
473 pub description: Option<String>,
474 pub value: Option<String>,
475 pub pressed: Option<bool>,
476 pub checked: Option<bool>,
477 pub expanded: Option<bool>,
478 pub disabled: bool,
479 pub hidden: bool,
480 pub level: Option<u8>,
481 pub shortcut: Option<String>,
482 pub focused: bool,
483 pub live: Option<String>,
484 pub atomic: bool,
485}
486
487impl AriaProperties {
488 pub fn new(role: AriaRole, label: impl Into<String>) -> Self {
489 Self {
490 role,
491 label: label.into(),
492 description: None,
493 value: None,
494 pressed: None,
495 checked: None,
496 expanded: None,
497 disabled: false,
498 hidden: false,
499 level: None,
500 shortcut: None,
501 focused: false,
502 live: None,
503 atomic: false,
504 }
505 }
506
507 pub fn description(mut self, d: impl Into<String>) -> Self {
508 self.description = Some(d.into());
509 self
510 }
511 pub fn value(mut self, v: impl Into<String>) -> Self {
512 self.value = Some(v.into());
513 self
514 }
515 pub fn checked(mut self, c: bool) -> Self {
516 self.checked = Some(c);
517 self
518 }
519 pub fn disabled(mut self, d: bool) -> Self {
520 self.disabled = d;
521 self
522 }
523 pub fn expanded(mut self, e: bool) -> Self {
524 self.expanded = Some(e);
525 self
526 }
527 pub fn level(mut self, l: u8) -> Self {
528 self.level = Some(l.clamp(1, 6));
529 self
530 }
531 pub fn shortcut(mut self, s: impl Into<String>) -> Self {
532 self.shortcut = Some(s.into());
533 self
534 }
535 pub fn focused(mut self, f: bool) -> Self {
536 self.focused = f;
537 self
538 }
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
547pub struct KeyModifiers {
548 pub shift: bool,
549 pub ctrl: bool,
550 pub alt: bool,
551 pub meta: bool,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct KeyShortcut {
557 pub key: String,
558 pub modifiers: KeyModifiers,
559 pub description: String,
560}
561
562impl KeyShortcut {
563 pub fn new(key: impl Into<String>, desc: impl Into<String>) -> Self {
564 Self {
565 key: key.into(),
566 modifiers: KeyModifiers::default(),
567 description: desc.into(),
568 }
569 }
570 pub fn with_ctrl(mut self) -> Self {
571 self.modifiers.ctrl = true;
572 self
573 }
574 pub fn with_shift(mut self) -> Self {
575 self.modifiers.shift = true;
576 self
577 }
578 pub fn with_alt(mut self) -> Self {
579 self.modifiers.alt = true;
580 self
581 }
582 pub fn with_meta(mut self) -> Self {
583 self.modifiers.meta = true;
584 self
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
594pub struct FocusableId(String);
595
596impl FocusableId {
597 pub fn as_str(&self) -> &str {
599 &self.0
600 }
601}
602
603impl From<&str> for FocusableId {
604 fn from(s: &str) -> Self {
605 Self(s.to_string())
606 }
607}
608impl From<String> for FocusableId {
609 fn from(s: String) -> Self {
610 Self(s)
611 }
612}
613
614#[derive(Debug, Clone)]
616pub struct FocusTrap {
617 pub id: FocusableId,
618 pub order: Vec<FocusableId>,
619 pub wrap: bool,
620}
621
622impl FocusTrap {
623 pub fn new(id: impl Into<FocusableId>, order: Vec<FocusableId>) -> Self {
624 Self {
625 id: id.into(),
626 order,
627 wrap: true,
628 }
629 }
630}
631
632#[derive(Debug, Default)]
634pub struct FocusManager {
635 order: Vec<FocusableId>,
636 order_set: HashSet<FocusableId>,
637 focused: Option<FocusableId>,
638 traps: Vec<FocusTrap>,
639}
640
641impl FocusManager {
642 pub fn new() -> Self {
643 Self::default()
644 }
645
646 pub fn register(&mut self, id: impl Into<FocusableId>) {
647 let id = id.into();
648 if self.order_set.insert(id.clone()) {
649 self.order.push(id);
650 }
651 }
652
653 pub fn unregister(&mut self, id: &FocusableId) {
654 if self.order_set.remove(id) {
655 self.order.retain(|x| x != id);
656 }
657 if self.focused.as_ref() == Some(id) {
658 self.focused = None;
659 }
660 }
661
662 pub fn focused(&self) -> Option<&FocusableId> {
663 self.focused.as_ref()
664 }
665
666 pub fn focus(&mut self, id: impl Into<FocusableId>) -> bool {
667 let id = id.into();
668 if self.order.contains(&id) || self.traps.iter().any(|t| t.order.contains(&id)) {
669 self.focused = Some(id);
670 true
671 } else {
672 false
673 }
674 }
675
676 pub fn focus_next(&mut self) -> Option<&FocusableId> {
677 let order = self.effective_order();
678 if order.is_empty() {
679 return None;
680 }
681 let idx = self
682 .focused
683 .as_ref()
684 .and_then(|f| order.iter().position(|x| x == f));
685 let next = match idx {
686 Some(i) if i + 1 < order.len() => &order[i + 1],
687 _ => &order[0],
688 };
689 self.focused = Some(next.clone());
690 self.focused.as_ref()
691 }
692
693 pub fn focus_prev(&mut self) -> Option<&FocusableId> {
694 let order = self.effective_order();
695 if order.is_empty() {
696 return None;
697 }
698 let idx = self
699 .focused
700 .as_ref()
701 .and_then(|f| order.iter().position(|x| x == f));
702 let prev = match idx {
703 Some(i) if i > 0 => &order[i - 1],
704 _ => &order[order.len() - 1],
705 };
706 self.focused = Some(prev.clone());
707 self.focused.as_ref()
708 }
709
710 pub fn push_trap(&mut self, trap: FocusTrap) -> FocusableId {
711 let id = trap.id.clone();
712 self.traps.push(trap);
713 id
714 }
715
716 pub fn pop_trap(&mut self) {
717 self.traps.pop();
718 }
719 pub fn trap_count(&self) -> usize {
720 self.traps.len()
721 }
722
723 fn effective_order(&self) -> &[FocusableId] {
724 self.traps
725 .last()
726 .map(|t| t.order.as_slice())
727 .unwrap_or(&self.order)
728 }
729}
730
731pub fn is_reduced_motion() -> bool {
740 AccessibilityPreferences::detect_from_system().reduce_motion
741}
742
743pub fn effective_duration(secs: f32) -> f32 {
745 if is_reduced_motion() { 0.0 } else { secs }
746}
747
748pub trait ErasedView: Send {
750 fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect);
751 fn name(&self) -> &'static str;
752 fn flex_weight_erased(&self) -> f32;
753 fn layout_erased(&self) -> Option<&dyn layout::LayoutView>;
754 fn grid_placement_erased(&self) -> Option<GridPlacement>;
755 fn clone_box(&self) -> Box<dyn ErasedView>;
756}
757
758impl<V: View + Clone + 'static> ErasedView for V {
759 fn render_erased(&self, renderer: &mut dyn Renderer, rect: Rect) {
760 self.render(renderer, rect);
761 }
762
763 fn name(&self) -> &'static str {
764 std::any::type_name::<V>()
765 }
766
767 fn flex_weight_erased(&self) -> f32 {
768 self.flex_weight()
769 }
770
771 fn layout_erased(&self) -> Option<&dyn layout::LayoutView> {
772 self.layout()
773 }
774
775 fn grid_placement_erased(&self) -> Option<GridPlacement> {
776 self.get_grid_placement()
777 }
778
779 fn clone_box(&self) -> Box<dyn ErasedView> {
780 Box::new(self.clone())
781 }
782}
783
784pub struct MemoView<V, F> {
787 id: u64,
788 data_hash: u64,
789 builder: F,
790 _v: std::marker::PhantomData<V>,
791}
792
793impl<V: View, F: Fn() -> V + Send + Sync> MemoView<V, F> {
794 pub fn new(id: u64, data_hash: u64, builder: F) -> Self {
796 Self {
797 id,
798 data_hash,
799 builder,
800 _v: std::marker::PhantomData,
801 }
802 }
803}
804
805impl<V: View + 'static, F: Fn() -> V + Send + Sync + 'static> View for MemoView<V, F> {
806 type Body = Never;
807 fn body(self) -> Self::Body {
808 unreachable!("MemoView does not have a body")
811 }
812
813 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
814 renderer.memoize(self.id, self.data_hash, &|r| {
815 let view = (self.builder)();
816 view.render(r, rect);
817 });
818 }
819}
820
821pub struct AnyView {
823 inner: Box<dyn ErasedView>,
824}
825
826impl Clone for AnyView {
827 fn clone(&self) -> Self {
828 Self {
829 inner: self.inner.clone_box(),
830 }
831 }
832}
833
834impl AnyView {
835 pub fn new<V: View + Clone + 'static>(view: V) -> Self {
836 Self {
837 inner: Box::new(view),
838 }
839 }
840}
841
842impl View for AnyView {
843 type Body = Never;
844 fn body(self) -> Self::Body {
845 unreachable!()
848 }
849
850 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
851 renderer.push_vnode(rect, self.inner.name());
852 self.inner.render_erased(renderer, rect);
853 renderer.pop_vnode();
854 }
855
856 fn flex_weight(&self) -> f32 {
857 self.inner.flex_weight_erased()
858 }
859
860 fn layout(&self) -> Option<&dyn layout::LayoutView> {
861 self.inner.layout_erased()
862 }
863
864 fn get_grid_placement(&self) -> Option<GridPlacement> {
865 self.inner.grid_placement_erased()
866 }
867}
868
869#[derive(Debug, Clone, PartialEq)]
873pub struct BifrostBridgeModifier {
874 pub id: String,
875}
876
877impl ViewModifier for BifrostBridgeModifier {
878 fn modify<V: View>(self, content: V) -> impl View {
879 ModifiedView::new(content, self)
880 }
881
882 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
883 renderer.register_shared_element(&self.id, rect);
885 }
886}
887
888#[derive(Debug, Clone, Copy, PartialEq)]
891pub struct MjolnirSliceModifier {
892 pub angle: f32,
893 pub offset: f32,
894}
895
896impl ViewModifier for MjolnirSliceModifier {
897 fn modify<V: View>(self, content: V) -> impl View {
898 ModifiedView::new(content, self)
899 }
900
901 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
902 renderer.push_mjolnir_slice(self.angle, self.offset);
903 }
904
905 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
906 renderer.pop_mjolnir_slice();
907 }
908}
909
910#[derive(Debug, Clone, Copy, PartialEq)]
913pub struct MjolnirShatterModifier {
914 pub pieces: u32,
915 pub force: f32,
916}
917
918impl ViewModifier for MjolnirShatterModifier {
919 fn modify<V: View>(self, content: V) -> impl View {
920 ModifiedView::new(content, self)
921 }
922
923 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
924 let pieces = self.pieces.max(1);
926 for i in 0..pieces {
927 let progress = i as f32 / pieces as f32;
928 let next_progress = (i + 1) as f32 / pieces as f32;
929
930 let angle_start = progress * 360.0;
931 let angle_end = next_progress * 360.0;
932
933 renderer.push_mjolnir_slice(angle_start, 0.0);
935 renderer.push_mjolnir_slice(angle_end + 180.0, 0.0);
936
937 let mid_angle = (angle_start + angle_end) / 2.0;
939 let rad = mid_angle.to_radians();
940 let dx = rad.cos() * self.force;
941 let dy = rad.sin() * self.force;
942
943 let shard_rect = Rect {
944 x: rect.x + dx,
945 y: rect.y + dy,
946 ..rect
947 };
948
949 view.render(renderer, shard_rect);
950
951 renderer.pop_mjolnir_slice();
952 renderer.pop_mjolnir_slice();
953 }
954 }
955}
956
957#[derive(Debug, Clone, Copy, PartialEq)]
960pub struct BifrostModifier {
961 pub blur: f32,
962 pub saturation: f32,
963 pub opacity: f32,
964 pub fresnel_strength: f32,
966}
967
968impl ViewModifier for BifrostModifier {
969 fn modify<V: View>(self, content: V) -> impl View {
970 ModifiedView::new(content, self)
971 }
972
973 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
974 if renderer.is_over_budget() {
975 renderer.bifrost(rect, self.blur * 0.5, self.saturation, self.opacity);
977 } else {
978 renderer.bifrost(rect, self.blur, self.saturation, self.opacity);
979 }
980 }
981}
982
983#[derive(Debug, Clone, Copy, PartialEq)]
985pub struct BackgroundModifier {
986 pub color: [f32; 4],
987}
988
989impl ViewModifier for BackgroundModifier {
990 fn modify<V: View>(self, content: V) -> impl View {
991 ModifiedView::new(content, self)
992 }
993
994 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
995 renderer.fill_rect(rect, self.color);
996 }
997}
998
999#[derive(Debug, Clone, Copy, PartialEq)]
1001pub struct PaddingModifier {
1002 pub amount: f32,
1003}
1004
1005impl ViewModifier for PaddingModifier {
1006 fn modify<V: View>(self, content: V) -> impl View {
1007 ModifiedView::new(content, self)
1008 }
1009
1010 fn transform_rect(&self, rect: Rect) -> Rect {
1011 Rect {
1012 x: rect.x + self.amount,
1013 y: rect.y + self.amount,
1014 width: (rect.width - 2.0 * self.amount).max(0.0),
1015 height: (rect.height - 2.0 * self.amount).max(0.0),
1016 }
1017 }
1018
1019 fn transform_proposal(&self, mut proposal: SizeProposal) -> SizeProposal {
1020 if let Some(w) = proposal.width {
1021 proposal.width = Some((w - 2.0 * self.amount).max(0.0));
1022 }
1023 if let Some(h) = proposal.height {
1024 proposal.height = Some((h - 2.0 * self.amount).max(0.0));
1025 }
1026 proposal
1027 }
1028
1029 fn transform_size(&self, mut size: Size) -> Size {
1030 size.width += 2.0 * self.amount;
1031 size.height += 2.0 * self.amount;
1032 size
1033 }
1034}
1035
1036#[derive(Debug, Clone, PartialEq)]
1039pub struct GungnirModifier {
1040 pub color: String,
1041 pub radius: f32,
1042 pub intensity: f32,
1043}
1044
1045impl ViewModifier for GungnirModifier {
1046 fn modify<V: View>(self, content: V) -> impl View {
1047 ModifiedView::new(content, self)
1048 }
1049
1050 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1051 renderer.stroke_rect(rect, [0.0, 1.0, 1.0, self.intensity], self.radius / 10.0);
1053 }
1054}
1055
1056#[derive(Debug, Clone, Copy, PartialEq)]
1058pub struct GungnirPulseModifier {
1059 pub color: [f32; 4],
1060 pub radius: f32,
1061 pub speed: f32,
1062}
1063
1064impl ViewModifier for GungnirPulseModifier {
1065 fn modify<V: View>(self, content: V) -> impl View {
1066 ModifiedView::new(content, self)
1067 }
1068
1069 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1070 let time = std::time::SystemTime::now()
1071 .duration_since(std::time::UNIX_EPOCH)
1072 .unwrap_or_default()
1073 .as_secs_f32();
1074
1075 let intensity = (time * self.speed).sin() * 0.5 + 0.5;
1078 let mut color = self.color;
1079 color[3] *= intensity;
1080
1081 renderer.stroke_rect(rect, color, self.radius);
1083 }
1084}
1085
1086#[derive(Debug, Clone, Copy, PartialEq)]
1088pub struct MagneticModifier {
1089 pub radius: f32,
1090 pub intensity: f32,
1091}
1092
1093impl ViewModifier for MagneticModifier {
1094 fn modify<V: View>(self, content: V) -> impl View {
1095 ModifiedView::new(content, self)
1096 }
1097
1098 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1099 let [px, py] = renderer.get_pointer_position();
1100 let center_x = rect.x + rect.width / 2.0;
1101 let center_y = rect.y + rect.height / 2.0;
1102
1103 let dx = px - center_x;
1104 let dy = py - center_y;
1105 let dist = (dx * dx + dy * dy).sqrt();
1106
1107 let mut offset_x = 0.0;
1108 let mut offset_y = 0.0;
1109
1110 if dist < self.radius && dist > 0.0 {
1111 let force = (1.0 - dist / self.radius) * self.intensity;
1112 offset_x = dx * force;
1113 offset_y = dy * force;
1114 }
1115
1116 let magnetic_rect = Rect {
1117 x: rect.x + offset_x,
1118 y: rect.y + offset_y,
1119 ..rect
1120 };
1121
1122 view.render(renderer, magnetic_rect);
1123 }
1124}
1125
1126#[derive(Debug, Clone, Copy, PartialEq)]
1129pub struct ManiGlowModifier {
1130 pub color: [f32; 4],
1131 pub radius: f32,
1132}
1133
1134impl ViewModifier for ManiGlowModifier {
1135 fn modify<V: View>(self, content: V) -> impl View {
1136 ModifiedView::new(content, self)
1137 }
1138
1139 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1140 if crate::load_system_state().realm == Realm::Asgard {
1141 renderer.mani_glow(rect, self.color, self.radius);
1142 }
1143 view.render(renderer, rect);
1144 }
1145}
1146
1147#[derive(Debug, Clone, Copy, PartialEq)]
1152pub struct BifrostLayerModifier {
1153 pub layer: MemoryLayer,
1154}
1155
1156impl ViewModifier for BifrostLayerModifier {
1157 fn modify<V: View>(self, content: V) -> impl View {
1158 ModifiedView::new(content, self)
1159 }
1160
1161 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1162 let realm = crate::load_system_state().realm;
1163 match self.layer {
1164 MemoryLayer::Episodic => {
1165 if realm == Realm::Asgard {
1166 renderer.bifrost(rect, 40.0, 1.2, 0.7);
1167 } else {
1168 renderer.fill_rect(rect, [0.1, 0.12, 0.15, 0.8]);
1169 }
1170 }
1171 MemoryLayer::Semantic => {
1172 if realm == Realm::Asgard {
1173 renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0, 0.6);
1174 } else {
1175 renderer.stroke_rect(rect, [0.4, 0.4, 0.4, 1.0], 1.5);
1176 }
1177 }
1178 MemoryLayer::Procedural => {
1179 renderer.fill_rect(rect, [0.05, 0.05, 0.07, 0.95]);
1180 let stroke_color = if realm == Realm::Asgard {
1181 [0.3, 0.3, 0.3, 1.0]
1182 } else {
1183 [0.2, 0.2, 0.2, 1.0]
1184 };
1185 renderer.stroke_rect(rect, stroke_color, 2.0);
1186 }
1187 }
1188 view.render(renderer, rect);
1189 }
1190}
1191
1192#[derive(Debug, Clone, Copy, PartialEq)]
1196pub struct FafnirModifier {
1197 pub id: u64,
1199}
1200
1201impl ViewModifier for FafnirModifier {
1202 fn modify<V: View>(self, content: V) -> impl View {
1203 ModifiedView::new(content, self)
1204 }
1205
1206 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1207 let state = crate::load_system_state();
1208 let vitality = state
1209 .get_component_state::<f32>(self.id)
1210 .map(|v| *v.read().unwrap())
1211 .unwrap_or(1.0);
1212
1213 let growth = (vitality - 1.0).clamp(0.0, 4.0);
1216 let scale = 1.0 + growth * 0.12;
1217 let glow_intensity = growth * 0.25;
1218
1219 let id = self.id;
1221 renderer.register_handler(
1222 "pointermove",
1223 std::sync::Arc::new(move |_| {
1224 crate::update_system_state(|s| {
1225 let mut s = s.clone();
1226 let v = s
1227 .get_component_state::<f32>(id)
1228 .map(|v| *v.read().unwrap())
1229 .unwrap_or(1.0);
1230 s.set_component_state(id, (v + 0.05).min(5.0)); s
1232 });
1233 }),
1234 );
1235
1236 if scale > 1.01 {
1237 renderer.push_transform([0.0, 0.0], [scale, scale], 0.0);
1238 }
1239
1240 if glow_intensity > 0.1 && state.realm == Realm::Asgard {
1241 renderer.gungnir(rect, [1.0, 0.84, 0.0, 1.0], 15.0 * vitality, glow_intensity);
1242 }
1243
1244 view.render(renderer, rect);
1245
1246 if scale > 1.01 {
1247 renderer.pop_transform();
1248 }
1249 }
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq)]
1254pub struct MimirIntentModifier;
1255
1256impl ViewModifier for MimirIntentModifier {
1257 fn modify<V: View>(self, content: V) -> impl View {
1258 ModifiedView::new(content, self)
1259 }
1260
1261 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1262 let state = crate::load_system_state();
1263 let pos = state.last_pointer_pos;
1264 let vel = state.pointer_velocity;
1265
1266 let center = [rect.x + rect.width / 2.0, rect.y + rect.height / 2.0];
1268 let dx = center[0] - pos[0];
1269 let dy = center[1] - pos[1];
1270
1271 let dot = vel[0] * dx + vel[1] * dy;
1273 let speed_sq = vel[0] * vel[0] + vel[1] * vel[1];
1274 let dist_sq = dx * dx + dy * dy;
1275
1276 if dot > 0.0 && dist_sq < 250.0 * 250.0 && speed_sq > 0.5 && state.realm == Realm::Asgard {
1277 let intent_strength = (dot / (speed_sq.sqrt() * dist_sq.sqrt())).clamp(0.0, 1.0);
1279 renderer.stroke_rect(rect, [0.0, 0.9, 1.0, 0.3 * intent_strength], 1.5);
1280 }
1281
1282 view.render(renderer, rect);
1283 }
1284}
1285
1286#[derive(Debug, Clone, Copy, PartialEq)]
1288pub struct KvasirVibeModifier {
1289 pub complexity: f32,
1290}
1291
1292impl ViewModifier for KvasirVibeModifier {
1293 fn modify<V: View>(self, content: V) -> impl View {
1294 ModifiedView::new(content, self)
1295 }
1296
1297 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1298 if crate::load_system_state().realm == Realm::Asgard {
1299 let t = renderer.elapsed_time();
1300 let c = self.complexity.clamp(0.0, 1.0);
1301
1302 let blur = 20.0 + c * 40.0;
1305 let turbulence_x = (t * (1.0 + c * 2.0)).sin() * 8.0 * c;
1306 let turbulence_y = (t * (0.8 + c * 1.5)).cos() * 5.0 * c;
1307 renderer.bifrost(
1308 rect.offset(turbulence_x, turbulence_y),
1309 blur,
1310 0.8 + c * 0.4,
1311 0.25,
1312 );
1313
1314 if c > 0.2 {
1316 let pulse = (t * (3.0 + c * 5.0)).sin().abs() * c;
1317 let color = [0.0, 0.9, 1.0, 0.4 * pulse]; renderer.gungnir(rect, color, 12.0 + c * 24.0, 0.6 * pulse);
1319 }
1320
1321 if c > 0.7 {
1323 let instability = (t * 15.0).cos().abs() * (c - 0.7) * 3.3;
1324 let warning_color = [1.0, 0.0, 0.4, 0.12 * instability];
1325 renderer.fill_rect(rect, warning_color);
1326 renderer.stroke_rect(rect, [1.0, 0.0, 0.2, 0.45 * instability], 1.8);
1327 }
1328 }
1329 view.render(renderer, rect);
1330 }
1331}
1332
1333#[derive(Debug, Clone, Copy, PartialEq)]
1335pub struct OdinsEyeModifier;
1336
1337impl ViewModifier for OdinsEyeModifier {
1338 fn modify<V: View>(self, content: V) -> impl View {
1339 ModifiedView::new(content, self)
1340 }
1341
1342 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1343 let state = crate::load_system_state();
1344 let t = renderer.elapsed_time();
1345
1346 view.render(renderer, rect);
1348
1349 if state.realm == Realm::Asgard {
1350 let eye_pulse = (t * 0.5).sin().abs() * 0.05;
1353 renderer.draw_radial_gradient(
1354 rect,
1355 [0.0, 0.6, 0.8, 0.08 + eye_pulse], [0.0, 0.0, 0.0, 0.0], );
1358
1359 let hugin_rect = Rect {
1361 x: rect.x + 20.0,
1362 y: rect.y + 40.0,
1363 width: 200.0,
1364 height: rect.height - 80.0,
1365 };
1366 renderer.draw_text(
1367 "HUGIN: THOUGHT",
1368 hugin_rect.x,
1369 hugin_rect.y,
1370 10.0,
1371 [0.0, 1.0, 1.0, 0.6],
1372 );
1373 for (i, thought) in state.thoughts.iter().rev().take(10).enumerate() {
1374 renderer.draw_text(
1375 thought,
1376 hugin_rect.x,
1377 hugin_rect.y + 20.0 + i as f32 * 14.0,
1378 9.0,
1379 [1.0, 1.0, 1.0, 0.4],
1380 );
1381 }
1382
1383 let munin_rect = Rect {
1385 x: rect.x + rect.width - 220.0,
1386 y: rect.y + 40.0,
1387 width: 200.0,
1388 height: rect.height - 80.0,
1389 };
1390 renderer.draw_text(
1391 "MUNIN: MEMORY",
1392 munin_rect.x,
1393 munin_rect.y,
1394 10.0,
1395 [1.0, 0.84, 0.0, 0.6],
1396 );
1397 for (i, node) in state.nodes.iter().take(10).enumerate() {
1398 let opacity = (node.weight.min(1.0)) * 0.5;
1399 renderer.draw_text(
1400 &node.id,
1401 munin_rect.x,
1402 munin_rect.y + 20.0 + i as f32 * 14.0,
1403 9.0,
1404 [1.0, 1.0, 1.0, opacity],
1405 );
1406 }
1407
1408 if let Some(focus_id) = &state.odin_focus {
1410 renderer.draw_text(
1412 &format!("EYE FOCUS: {}", focus_id),
1413 rect.x + rect.width / 2.0 - 50.0,
1414 rect.y + 20.0,
1415 12.0,
1416 [0.0, 1.0, 1.0, 0.8],
1417 );
1418
1419 renderer.gungnir(
1422 Rect {
1423 x: rect.x + rect.width / 2.0 - 1.0,
1424 y: rect.y,
1425 width: 2.0,
1426 height: rect.height,
1427 },
1428 [0.0, 1.0, 1.0, 1.0],
1429 20.0,
1430 0.4,
1431 );
1432 }
1433 }
1434 }
1435}
1436
1437#[derive(Debug, Clone, Copy, PartialEq)]
1439pub struct SleipnirParams {
1440 pub stiffness: f32,
1441 pub damping: f32,
1442 pub mass: f32,
1443}
1444
1445impl SleipnirParams {
1446 pub fn snappy() -> Self {
1447 Self {
1448 stiffness: 230.0,
1449 damping: 22.0,
1450 mass: 1.0,
1451 }
1452 }
1453 pub fn fluid() -> Self {
1454 Self {
1455 stiffness: 170.0,
1456 damping: 26.0,
1457 mass: 1.0,
1458 }
1459 }
1460 pub fn heavy() -> Self {
1461 Self {
1462 stiffness: 90.0,
1463 damping: 20.0,
1464 mass: 1.0,
1465 }
1466 }
1467 pub fn bouncy() -> Self {
1468 Self {
1469 stiffness: 190.0,
1470 damping: 14.0,
1471 mass: 1.0,
1472 }
1473 }
1474}
1475
1476impl Default for SleipnirParams {
1477 fn default() -> Self {
1478 Self::fluid()
1479 }
1480}
1481
1482#[derive(Debug, Clone, Copy, PartialEq)]
1483struct SolverState {
1484 x: f32,
1485 v: f32,
1486}
1487
1488#[derive(Debug, Clone, Copy, PartialEq)]
1491pub struct SleipnirSolver {
1492 params: SleipnirParams,
1493 target: f32,
1494 state: SolverState,
1495}
1496
1497impl SleipnirSolver {
1498 pub fn new(params: SleipnirParams, target: f32, current: f32) -> Self {
1500 Self {
1501 params,
1502 target,
1503 state: SolverState { x: current, v: 0.0 },
1504 }
1505 }
1506
1507 pub fn tick(&mut self, dt: f32) -> f32 {
1509 if dt <= 0.0 {
1510 return self.state.x;
1511 }
1512
1513 let mut remaining = dt;
1515 let step = 1.0 / 120.0;
1516
1517 while remaining > 0.0 {
1518 let d = remaining.min(step);
1519 self.step(d);
1520 remaining -= d;
1521 }
1522
1523 self.state.x
1524 }
1525
1526 fn step(&mut self, dt: f32) {
1527 let a = self.evaluate(self.state, 0.0, SolverState { x: 0.0, v: 0.0 });
1528 let b = self.evaluate(self.state, dt * 0.5, a);
1529 let c = self.evaluate(self.state, dt * 0.5, b);
1530 let d = self.evaluate(self.state, dt, c);
1531
1532 let dxdt = 1.0 / 6.0 * (a.x + 2.0 * (b.x + c.x) + d.x);
1533 let dvdt = 1.0 / 6.0 * (a.v + 2.0 * (b.v + c.v) + d.v);
1534
1535 self.state.x += dxdt * dt;
1536 self.state.v += dvdt * dt;
1537 }
1538
1539 fn evaluate(&self, initial: SolverState, dt: f32, d: SolverState) -> SolverState {
1540 let state = SolverState {
1541 x: initial.x + d.x * dt,
1542 v: initial.v + d.v * dt,
1543 };
1544 let force =
1545 -self.params.stiffness * (state.x - self.target) - self.params.damping * state.v;
1546 let mass = self.params.mass.max(0.001);
1547 SolverState {
1548 x: state.v,
1549 v: force / mass,
1550 }
1551 }
1552
1553 pub fn is_settled(&self) -> bool {
1554 (self.state.x - self.target).abs() < 0.001 && self.state.v.abs() < 0.001
1555 }
1556
1557 pub fn set_target(&mut self, target: f32) {
1558 self.target = target;
1559 }
1560
1561 pub fn current_value(&self) -> f32 {
1562 self.state.x
1563 }
1564}
1565
1566#[derive(Debug, Clone, PartialEq)]
1568pub struct SleipnirModifier {
1569 pub id: u64,
1570 pub target: f32,
1571 pub params: SleipnirParams,
1572}
1573
1574impl ViewModifier for SleipnirModifier {
1575 fn modify<V: View>(self, content: V) -> impl View {
1576 ModifiedView::new(content, self)
1577 }
1578
1579 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1580 let state = load_system_state();
1581
1582 let solver_lock_opt = state.get_component_state::<SleipnirSolver>(self.id);
1584
1585 let current_val;
1586
1587 if let Some(lock) = solver_lock_opt {
1588 let mut solver = lock.write().unwrap();
1590 solver.set_target(self.target);
1591 current_val = solver.tick(renderer.delta_time());
1592
1593 if !solver.is_settled() {
1595 renderer.request_redraw();
1596 }
1597 } else {
1598 let solver = SleipnirSolver::new(
1600 self.params,
1601 self.target,
1602 self.target, );
1604
1605 get_system_state().rcu(|old| {
1607 let mut new_state = (**old).clone();
1608 new_state.set_component_state(self.id, solver);
1609 new_state
1610 });
1611
1612 current_val = self.target;
1613 }
1614
1615 renderer.push_transform([0.0, current_val], [1.0, 1.0], 0.0);
1617 view.render(renderer, rect);
1618 renderer.pop_transform();
1619 }
1620}
1621
1622#[derive(Debug, Clone, Copy, PartialEq)]
1625pub struct TransformModifier {
1626 pub translation: [f32; 2],
1627 pub scale: [f32; 2],
1628 pub rotation: f32,
1629}
1630
1631impl Default for TransformModifier {
1632 fn default() -> Self {
1633 Self::new()
1634 }
1635}
1636
1637impl TransformModifier {
1638 pub fn new() -> Self {
1639 Self {
1640 translation: [0.0, 0.0],
1641 scale: [1.0, 1.0],
1642 rotation: 0.0,
1643 }
1644 }
1645
1646 pub fn translate(mut self, x: f32, y: f32) -> Self {
1647 self.translation = [x, y];
1648 self
1649 }
1650
1651 pub fn scale(mut self, x: f32, y: f32) -> Self {
1652 self.scale = [x, y];
1653 self
1654 }
1655
1656 pub fn rotate(mut self, radians: f32) -> Self {
1657 self.rotation = radians;
1658 self
1659 }
1660}
1661
1662impl ViewModifier for TransformModifier {
1663 fn modify<V: View>(self, content: V) -> impl View {
1664 ModifiedView::new(content, self)
1665 }
1666
1667 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
1668 renderer.push_transform(self.translation, self.scale, self.rotation);
1669 view.render(renderer, rect);
1670 renderer.pop_transform();
1671 }
1672}
1673
1674#[derive(Clone)]
1677pub struct LifecycleModifier {
1678 pub on_appear: Option<Arc<dyn Fn() + Send + Sync>>,
1679 pub on_disappear: Option<Arc<dyn Fn() + Send + Sync>>,
1680}
1681
1682impl ViewModifier for LifecycleModifier {
1683 fn modify<V: View>(self, content: V) -> impl View {
1684 ModifiedView::new(content, self)
1685 }
1686}
1687
1688#[derive(Debug, Clone, Copy, PartialEq)]
1691pub struct OpacityModifier {
1692 pub opacity: f32,
1693}
1694
1695impl ViewModifier for OpacityModifier {
1696 fn modify<V: View>(self, content: V) -> impl View {
1697 ModifiedView::new(content, self)
1698 }
1699
1700 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1701 renderer.push_opacity(self.opacity);
1702 }
1703
1704 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1705 renderer.pop_opacity();
1706 }
1707}
1708
1709#[derive(Clone)]
1711pub struct OnClickModifier {
1712 pub action: Arc<dyn Fn() + Send + Sync>,
1713}
1714
1715impl ViewModifier for OnClickModifier {
1716 fn modify<V: View>(self, content: V) -> impl View {
1717 ModifiedView::new(content, self)
1718 }
1719
1720 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1721 let action = self.action.clone();
1722 renderer.register_handler(
1723 "pointerclick",
1724 std::sync::Arc::new(move |event| {
1725 if let Event::PointerClick { .. } = event {
1726 (action)();
1727 }
1728 }),
1729 );
1730 }
1731}
1732
1733#[derive(Clone)]
1735pub struct OnPointerEnterModifier {
1736 pub action: Arc<dyn Fn() + Send + Sync>,
1737}
1738
1739impl ViewModifier for OnPointerEnterModifier {
1740 fn modify<V: View>(self, content: V) -> impl View {
1741 ModifiedView::new(content, self)
1742 }
1743
1744 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1745 let action = self.action.clone();
1746 renderer.register_handler(
1747 "pointerenter",
1748 std::sync::Arc::new(move |event| {
1749 if let Event::PointerEnter = event {
1750 (action)();
1751 }
1752 }),
1753 );
1754 }
1755}
1756
1757#[derive(Clone)]
1759pub struct OnPointerLeaveModifier {
1760 pub action: Arc<dyn Fn() + Send + Sync>,
1761}
1762
1763impl ViewModifier for OnPointerLeaveModifier {
1764 fn modify<V: View>(self, content: V) -> impl View {
1765 ModifiedView::new(content, self)
1766 }
1767
1768 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1769 let action = self.action.clone();
1770 renderer.register_handler(
1771 "pointerleave",
1772 std::sync::Arc::new(move |event| {
1773 if let Event::PointerLeave = event {
1774 (action)();
1775 }
1776 }),
1777 );
1778 }
1779}
1780
1781#[derive(Clone)]
1783pub struct OnPointerMoveModifier {
1784 pub action: Arc<dyn Fn(f32, f32) + Send + Sync>,
1785}
1786
1787impl ViewModifier for OnPointerMoveModifier {
1788 fn modify<V: View>(self, content: V) -> impl View {
1789 ModifiedView::new(content, self)
1790 }
1791
1792 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1793 let action = self.action.clone();
1794 renderer.register_handler(
1795 "pointermove",
1796 std::sync::Arc::new(move |event| {
1797 if let Event::PointerMove { x, y, .. } = event {
1798 (action)(x, y);
1799 }
1800 }),
1801 );
1802 }
1803}
1804
1805#[derive(Clone)]
1807pub struct OnPointerDownModifier {
1808 pub action: Arc<dyn Fn() + Send + Sync>,
1809}
1810
1811impl ViewModifier for OnPointerDownModifier {
1812 fn modify<V: View>(self, content: V) -> impl View {
1813 ModifiedView::new(content, self)
1814 }
1815
1816 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1817 let action = self.action.clone();
1818 renderer.register_handler(
1819 "pointerdown",
1820 std::sync::Arc::new(move |event| {
1821 if let Event::PointerDown { .. } = event {
1822 (action)();
1823 }
1824 }),
1825 );
1826 }
1827}
1828
1829#[derive(Clone)]
1831pub struct OnPointerUpModifier {
1832 pub action: Arc<dyn Fn() + Send + Sync>,
1833}
1834
1835impl ViewModifier for OnPointerUpModifier {
1836 fn modify<V: View>(self, content: V) -> impl View {
1837 ModifiedView::new(content, self)
1838 }
1839
1840 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1841 let action = self.action.clone();
1842 renderer.register_handler(
1843 "pointerup",
1844 std::sync::Arc::new(move |event| {
1845 if let Event::PointerUp { .. } = event {
1846 (action)();
1847 }
1848 }),
1849 );
1850 }
1851}
1852
1853#[derive(Debug, Clone, Copy, PartialEq)]
1856pub struct ForegroundColorModifier {
1857 pub color: [f32; 4],
1858}
1859
1860impl ViewModifier for ForegroundColorModifier {
1861 fn modify<V: View>(self, content: V) -> impl View {
1862 ModifiedView::new(content, self)
1863 }
1864}
1865
1866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1869pub struct ClipModifier;
1870
1871impl ViewModifier for ClipModifier {
1872 fn modify<V: View>(self, content: V) -> impl View {
1873 ModifiedView::new(content, self)
1874 }
1875
1876 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1877 renderer.push_clip_rect(rect);
1878 }
1879
1880 fn post_render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
1881 renderer.pop_clip_rect();
1882 }
1883}
1884
1885#[derive(Debug, Clone, Copy, PartialEq)]
1887pub struct BorderModifier {
1888 pub color: [f32; 4],
1889 pub width: f32,
1890}
1891
1892impl ViewModifier for BorderModifier {
1893 fn modify<V: View>(self, content: V) -> impl View {
1894 ModifiedView::new(content, self)
1895 }
1896
1897 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1898 renderer.stroke_rect(rect, self.color, self.width);
1899 }
1900}
1901
1902#[doc(hidden)]
1904pub enum Never {}
1905
1906impl View for Never {
1907 type Body = Never;
1908 fn body(self) -> Never {
1909 unreachable!()
1912 }
1913}
1914
1915#[derive(Debug, Clone, Copy, Default)]
1917pub struct EmptyView;
1918
1919impl View for EmptyView {
1920 type Body = Never;
1921 fn body(self) -> Self::Body {
1922 unreachable!()
1925 }
1926 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1927 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
1928 Size {
1929 width: 0.0,
1930 height: 0.0,
1931 }
1932 }
1933}
1934
1935#[derive(Clone)]
1938pub struct ModifiedView<V, M> {
1939 view: V,
1940 modifier: M,
1941}
1942
1943impl<V: View, M: ViewModifier> ModifiedView<V, M> {
1944 #[doc(hidden)]
1945 pub fn new(view: V, modifier: M) -> Self {
1946 Self { view, modifier }
1947 }
1948}
1949
1950impl<V: View, M: ViewModifier> View for ModifiedView<V, M> {
1951 type Body = ModifiedView<V::Body, M>;
1952
1953 fn body(self) -> Self::Body {
1954 ModifiedView {
1955 view: self.view.body(),
1956 modifier: self.modifier.clone(),
1957 }
1958 }
1959
1960 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
1961 self.modifier.render_view(&self.view, renderer, rect);
1962 }
1963
1964 fn intrinsic_size(&self, renderer: &mut dyn Renderer, proposal: SizeProposal) -> Size {
1965 self.modifier.measure_view(&self.view, renderer, proposal)
1966 }
1967
1968 fn flex_weight(&self) -> f32 {
1969 self.modifier.child_flex_weight(&self.view)
1970 }
1971
1972 fn layout(&self) -> Option<&dyn layout::LayoutView> {
1973 self.modifier.layout().or_else(|| self.view.layout())
1974 }
1975
1976 fn get_grid_placement(&self) -> Option<GridPlacement> {
1977 self.modifier
1978 .get_grid_placement()
1979 .or_else(|| self.view.get_grid_placement())
1980 }
1981}
1982
1983pub trait ViewModifier: Send + Clone {
1984 fn modify<V: View>(self, content: V) -> impl View;
1985
1986 fn get_grid_placement(&self) -> Option<GridPlacement> {
1988 None
1989 }
1990
1991 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1993
1994 fn post_render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {}
1996
1997 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
2000 self.render(renderer, rect);
2001 let child_rect = self.transform_rect(rect);
2002 view.render(renderer, child_rect);
2003 self.post_render(renderer, rect);
2004 }
2005
2006 fn transform_rect(&self, rect: Rect) -> Rect {
2007 rect
2008 }
2009
2010 fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
2012 proposal
2013 }
2014
2015 fn transform_size(&self, size: Size) -> Size {
2017 size
2018 }
2019
2020 fn measure_view<V: View>(
2022 &self,
2023 view: &V,
2024 renderer: &mut dyn Renderer,
2025 proposal: SizeProposal,
2026 ) -> Size {
2027 let child_proposal = self.transform_proposal(proposal);
2028 let child_size = view.intrinsic_size(renderer, child_proposal);
2029 self.transform_size(child_size)
2030 }
2031
2032 fn child_flex_weight<V: View>(&self, view: &V) -> f32 {
2034 view.flex_weight()
2035 }
2036
2037 fn layout(&self) -> Option<&dyn layout::LayoutView> {
2038 None
2039 }
2040}
2041
2042#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2053pub struct RenderStateSnapshot {
2054 pub clip_depth: u32,
2055 pub opacity_depth: u32,
2056 pub slice_depth: u32,
2057 pub shadow_depth: u32,
2058 pub transform_depth: u32,
2059 pub vnode_depth: u32,
2060}
2061
2062#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
2064pub struct TelemetryData {
2065 pub frame_time_ms: f32,
2066 pub frame_budget_ms: f32,
2068 pub frame_budget_remaining_ms: f32,
2070 pub layout_budget_remaining_ms: f32,
2072 pub frame_over_budget: bool,
2074 pub layout_over_budget: bool,
2076 pub p99_frame_time_ms: f32,
2078 pub frame_jitter_ms: f32,
2080 pub hardware_stall_detected: bool,
2082
2083 pub input_time_ms: f32,
2085 pub state_flush_time_ms: f32,
2086 pub layout_time_ms: f32,
2087 pub draw_time_ms: f32,
2088 pub gpu_submit_time_ms: f32,
2089
2090 pub draw_calls: u32,
2091 pub vertices: u32,
2092
2093 pub berserker_rage: f32,
2095
2096 pub vram_usage_mb: f32,
2098 pub vram_textures_mb: f32,
2099 pub vram_buffers_mb: f32,
2100 pub vram_pipelines_mb: f32,
2101 pub vram_exhausted: bool,
2103}
2104
2105#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
2107pub struct FrameBudget {
2108 pub target_ms: f32,
2110 pub allow_degradation: bool,
2113}
2114
2115impl Default for FrameBudget {
2116 fn default() -> Self {
2117 Self {
2118 target_ms: 16.0,
2119 allow_degradation: true,
2120 }
2121 }
2122}
2123
2124pub trait ElapsedTime {
2132 fn elapsed_time(&self) -> f32;
2134
2135 fn delta_time(&self) -> f32;
2137}
2138
2139pub trait Renderer: ElapsedTime + Send {
2151 fn request_redraw(&mut self) {}
2154
2155 fn is_over_budget(&self) -> bool {
2158 false
2159 }
2160
2161 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]);
2163 fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]);
2164 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]);
2166
2167 fn draw_background_image(&mut self, image_name: &str, rect: Rect) {
2172 Renderer::draw_image(self, image_name, rect);
2173 }
2174
2175 fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
2179 let _ = (rect, radius, blur_radius);
2181 }
2182 fn fill_glass_rect_with_intensity(&mut self, rect: Rect, radius: f32, blur_radius: f32, glass_intensity: f32) {
2185 let _ = (rect, radius, blur_radius, glass_intensity);
2186 }
2187 fn fill_glass_rect_with_tint(&mut self, rect: Rect, radius: f32, blur_radius: f32, tint_color: [f32; 4], glass_intensity: f32) {
2190 let _ = (rect, radius, blur_radius, tint_color, glass_intensity);
2192 }
2193 fn fill_glass_rect_with_pressure(&mut self, rect: Rect, radius: f32, blur_radius: f32, pressure: f32) {
2198 Renderer::fill_glass_rect_with_intensity(self, rect, radius, blur_radius, pressure);
2200 }
2201
2202 fn fill_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4]) {
2205 Renderer::fill_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color);
2207 }
2208
2209 fn stroke_squircle(&mut self, rect: Rect, _n: f32, color: [f32; 4], stroke_width: f32) {
2211 Renderer::stroke_rounded_rect(self, rect, rect.width.min(rect.height) * 0.22, color, stroke_width);
2212 }
2213
2214 fn draw_focus_ring(&mut self, rect: Rect, radius: f32, offset: f32, width: f32, color: [f32; 4]) {
2217 let ring_rect = Rect {
2219 x: rect.x - offset,
2220 y: rect.y - offset,
2221 width: rect.width + 2.0 * offset,
2222 height: rect.height + 2.0 * offset,
2223 };
2224 Renderer::stroke_rounded_rect(self, ring_rect, radius + offset, color, width);
2225 }
2226
2227 fn draw_3d_cube(&mut self, _rect: Rect, _color: [f32; 4], _rotation: [f32; 3]) {}
2230
2231 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2233 fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32);
2234 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32);
2236 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32);
2238 fn fill_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4]) {}
2240 fn stroke_polygon(&mut self, _vertices: &[[f32; 2]], _color: [f32; 4], _stroke_width: f32) {}
2242
2243 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
2245 let span = cvkg_runic_text::TextSpan::new(
2246 text,
2247 cvkg_runic_text::TextStyle {
2248 family: "Inter".to_string(),
2249 font_size: size,
2250 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],
2251 fallback_families: vec![
2252 "SF Pro".to_string(),
2253 "SF Pro Text".to_string(),
2254 "Helvetica Neue".to_string(),
2255 "Helvetica".to_string(),
2256 "Arial".to_string(),
2257 "sans-serif".to_string(),
2258 ],
2259 ..Default::default()
2260 },
2261 );
2262 if let Some(shaped) = Renderer::shape_rich_text(
2263 self,
2264 &[span],
2265 None,
2266 cvkg_runic_text::TextAlign::Start,
2267 cvkg_runic_text::TextOverflow::Visible,
2268 ) {
2269 Renderer::draw_shaped_text(self, &shaped, x, y);
2270 }
2271 }
2272
2273 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
2275 let span = cvkg_runic_text::TextSpan::new(
2276 text,
2277 cvkg_runic_text::TextStyle {
2278 family: "Inter".to_string(),
2279 font_size: size,
2280 fallback_families: vec![
2281 "SF Pro".to_string(),
2282 "SF Pro Text".to_string(),
2283 "Helvetica Neue".to_string(),
2284 "Helvetica".to_string(),
2285 "Arial".to_string(),
2286 "sans-serif".to_string(),
2287 ],
2288 ..Default::default()
2289 },
2290 );
2291 if let Some(shaped) = Renderer::shape_rich_text(
2292 self,
2293 &[span],
2294 None,
2295 cvkg_runic_text::TextAlign::Start,
2296 cvkg_runic_text::TextOverflow::Visible,
2297 ) {
2298 let scale = self.text_scale_factor().max(1.0);
2299 (shaped.width / scale, shaped.height / scale)
2300 } else {
2301 (0.0, 0.0)
2302 }
2303 }
2304
2305 fn measure_text_baseline(&mut self, text: &str, size: f32) -> f32 {
2309 let span = cvkg_runic_text::TextSpan::new(
2310 text,
2311 cvkg_runic_text::TextStyle {
2312 family: "Inter".to_string(),
2313 font_size: size,
2314 fallback_families: vec![
2315 "SF Pro".to_string(),
2316 "SF Pro Text".to_string(),
2317 "Helvetica Neue".to_string(),
2318 "Helvetica".to_string(),
2319 "Arial".to_string(),
2320 "sans-serif".to_string(),
2321 ],
2322 ..Default::default()
2323 },
2324 );
2325 if let Some(shaped) = Renderer::shape_rich_text(
2326 self,
2327 &[span],
2328 None,
2329 cvkg_runic_text::TextAlign::Start,
2330 cvkg_runic_text::TextOverflow::Visible,
2331 ) {
2332 shaped.ascent / self.text_scale_factor().max(1.0)
2333 } else {
2334 0.0
2335 }
2336 }
2337
2338 fn text_scale_factor(&self) -> f32 {
2343 1.0
2344 }
2345
2346 fn shape_rich_text(
2347 &mut self,
2348 _spans: &[cvkg_runic_text::TextSpan],
2349 _max_width: Option<f32>,
2350 _align: cvkg_runic_text::TextAlign,
2351 _overflow: cvkg_runic_text::TextOverflow,
2352 ) -> Option<cvkg_runic_text::ShapedText> {
2353 None
2354 }
2355
2356 fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {}
2357
2358 fn draw_texture(&mut self, _texture_id: u32, _rect: Rect) {}
2361 fn draw_image(&mut self, _image_name: &str, _rect: Rect) {}
2363 fn load_image(&mut self, _name: &str, _data: &[u8]) {}
2365 fn prewarm_vram(&mut self, _assets: Vec<(String, Vec<u8>)>) {}
2368
2369 fn get_pointer_position(&self) -> [f32; 2] {
2371 [0.0, 0.0]
2372 }
2373
2374 fn upload_data_texture(&mut self, _id: &str, _data: &[f32], _width: u32, _height: u32) {}
2377 fn draw_heatmap(&mut self, _texture_id: &str, _rect: Rect, _palette: &str) {}
2379
2380 fn draw_mesh(&mut self, _mesh: &Mesh, _color: [f32; 4], _transform: glam::Mat4) {}
2383
2384 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
2386
2387 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
2390
2391 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
2394
2395 fn pop_transform_3d(&mut self) {}
2397
2398 fn render_scene_node_3d(
2407 &mut self,
2408 _position: [f32; 3],
2409 _rotation: [f32; 4],
2410 _scale: [f32; 3],
2411 _color: [f32; 4],
2412 _meshes: &[Mesh],
2413 ) {
2414 }
2416
2417 fn draw_linear_gradient(
2419 &mut self,
2420 _rect: Rect,
2421 _start_color: [f32; 4],
2422 _end_color: [f32; 4],
2423 _angle: f32,
2424 ) {
2425 }
2426 fn draw_radial_gradient(
2428 &mut self,
2429 _rect: Rect,
2430 _inner_color: [f32; 4],
2431 _outer_color: [f32; 4],
2432 ) {
2433 }
2434 fn draw_drop_shadow(
2436 &mut self,
2437 _rect: Rect,
2438 _radius: f32,
2439 _color: [f32; 4],
2440 _blur: f32,
2441 _spread: f32,
2442 ) {
2443 }
2444 fn stroke_dashed_rounded_rect(
2446 &mut self,
2447 _rect: Rect,
2448 _radius: f32,
2449 _color: [f32; 4],
2450 _width: f32,
2451 _dash: f32,
2452 _gap: f32,
2453 ) {
2454 }
2455 fn draw_9slice(
2457 &mut self,
2458 _image_name: &str,
2459 _rect: Rect,
2460 _left: f32,
2461 _top: f32,
2462 _right: f32,
2463 _bottom: f32,
2464 ) {
2465 }
2466
2467 fn push_clip_rect(&mut self, _rect: Rect) {}
2471 fn pop_clip_rect(&mut self) {}
2473 fn current_clip_rect(&self) -> Rect {
2476 Rect::new(-10000.0, -10000.0, 20000.0, 20000.0)
2477 }
2478
2479 fn push_opacity(&mut self, _opacity: f32) {}
2483 fn pop_opacity(&mut self) {}
2485
2486 fn set_theme(&mut self, _theme: ColorTheme) {}
2488 fn set_rage(&mut self, _rage: f32) {}
2489 fn set_berserker_mode(&mut self, _state: BerserkerMode) {}
2490 fn trigger_shatter_event(&mut self, _origin: [f32; 2], _force: f32) {}
2491 fn set_fireball_pos(&mut self, _pos: [f32; 2]) {}
2493 fn set_scene(&mut self, _scene: &str) {}
2495 fn set_scene_by_name(&mut self, name: &str) {
2499 if let Some(preset) = resolve_scene_by_name(name) {
2500 Renderer::set_scene_preset(self, preset);
2501 }
2502 }
2503
2504 fn capture_png(&mut self) -> Vec<u8> {
2507 Vec::new()
2508 }
2509 fn print(&mut self) {}
2511
2512 fn set_scene_preset(&mut self, _preset: u32) {}
2513
2514 fn bifrost(&mut self, _rect: Rect, _blur: f32, _saturation: f32, _opacity: f32) {}
2517 fn gungnir(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2519 fn gungnir_soft(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32, _intensity: f32) {}
2521 fn set_default_background_color(&mut self, _color: [f32; 4]) {}
2524 fn mani_glow(&mut self, _rect: Rect, _color: [f32; 4], _radius: f32) {}
2526 fn push_mjolnir_slice(&mut self, _angle: f32, _offset: f32) {}
2528 fn pop_mjolnir_slice(&mut self) {}
2529 fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer));
2533 fn snapshot_render_state(&self) -> RenderStateSnapshot {
2538 RenderStateSnapshot::default()
2539 }
2540 fn restore_render_state(&mut self, _snap: RenderStateSnapshot) {}
2547 fn mjolnir_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2549 fn mjolnir_fluid_shatter(&mut self, _rect: Rect, _pieces: u32, _force: f32, _color: [f32; 4]) {}
2550 fn draw_mjolnir_bolt(&mut self, _from: [f32; 2], _to: [f32; 2], _color: [f32; 4]) {}
2551
2552 fn dispatch_particles(
2555 &mut self,
2556 _origin: [f32; 2],
2557 _count: u32,
2558 _effect_type: &str,
2559 _color: [f32; 4],
2560 ) {
2561 }
2562
2563 fn draw_hologram(&mut self, _rect: Rect, _hologram_id: &str, _time: f32) {}
2565
2566 fn set_aria_role(&mut self, _role: &str) {}
2568 fn set_aria_label(&mut self, _label: &str) {}
2569 fn set_aria_valuemin(&mut self, _min: f32) {}
2570 fn set_aria_valuemax(&mut self, _max: f32) {}
2571 fn set_aria_valuenow(&mut self, _now: f32) {}
2572
2573 fn register_shared_element(&mut self, _id: &str, _rect: Rect) {}
2575
2576 fn set_key(&mut self, _key: &str) {}
2578
2579 fn get_telemetry(&self) -> TelemetryData {
2582 TelemetryData::default()
2583 }
2584
2585 fn push_shadow(&mut self, _radius: f32, _color: [f32; 4], _offset: [f32; 2]) {}
2588 fn pop_shadow(&mut self) {}
2590
2591 fn push_vnode(&mut self, _rect: Rect, _name: &'static str) {}
2594 fn pop_vnode(&mut self) {}
2596 fn register_handler(
2598 &mut self,
2599 _event_type: &str,
2600 _handler: std::sync::Arc<dyn Fn(Event) + Send + Sync>,
2601 ) {
2602 }
2603
2604 fn set_z_index(&mut self, _z: f32) {}
2608 fn get_z_index(&self) -> f32 {
2610 0.0
2611 }
2612
2613 fn load_svg(&mut self, _name: &str, _svg_data: &[u8]) {}
2616 fn draw_svg(&mut self, _name: &str, _rect: Rect) {}
2618 fn draw_svg_with_offset(&mut self, name: &str, rect: Rect, _animation_time_offset: f32) {
2622 Renderer::draw_svg(self, name, rect);
2623 }
2624 fn draw_svg_with_order(&mut self, name: &str, rect: Rect, _draw_order: i32) {
2627 Renderer::draw_svg(self, name, rect);
2628 }
2629 fn serialize_svg(&mut self, _name: &str) -> Result<String, String> {
2633 Err("SVG serialization not supported by this renderer".into())
2634 }
2635 fn apply_svg_filter(
2639 &mut self,
2640 _name: &str,
2641 _filter_id: &str,
2642 _region: Rect,
2643 ) -> Result<String, String> {
2644 Err("SVG filter not supported by this renderer".into())
2645 }
2646
2647 fn push_transform(&mut self, _translation: [f32; 2], _scale: [f32; 2], _rotation: f32) {}
2652 fn push_affine(&mut self, _transform: [f32; 6]) {}
2655 fn pop_transform(&mut self) {}
2657 fn query_layout(&self, _node_id: scene_graph::NodeId) -> Option<Rect> {
2659 None
2660 }
2661 fn set_debug_layout(&mut self, _enabled: bool) {}
2663 fn get_debug_layout(&self) -> bool {
2665 false
2666 }
2667
2668 fn set_material(&mut self, _material: crate::material::DrawMaterial) {}
2672 fn current_material(&self) -> crate::material::DrawMaterial {
2674 crate::material::DrawMaterial::Opaque
2675 }
2676
2677 fn mimir_intent(&self) -> [f32; 2] {
2680 [0.0, 0.0]
2681 }
2682 fn magnetic_warp(&self, pointer: [f32; 2], anchor_rect: Rect, strength: f32) -> [f32; 2] {
2684 if strength <= 0.0 {
2685 return pointer;
2686 }
2687 let cx = anchor_rect.x + anchor_rect.width / 2.0;
2688 let cy = anchor_rect.y + anchor_rect.height / 2.0;
2689 let dx = pointer[0] - cx;
2690 let dy = pointer[1] - cy;
2691 let dist = (dx * dx + dy * dy).sqrt();
2692 let radius = 120.0;
2693 if dist < radius && dist > 0.0 {
2694 let force = (1.0 - dist / radius) * strength;
2695 [pointer[0] - dx * force, pointer[1] - dy * force]
2696 } else {
2697 pointer
2698 }
2699 }
2700 fn mani_glow_intensity(&self, pointer: [f32; 2], bounds: Rect, radius: f32) -> f32 {
2702 let cx = bounds.x + bounds.width / 2.0;
2703 let cy = bounds.y + bounds.height / 2.0;
2704 let dist = ((pointer[0] - cx).powi(2) + (pointer[1] - cy).powi(2)).sqrt();
2705 if dist < radius {
2706 (1.0 - dist / radius).clamp(0.0, 1.0)
2707 } else {
2708 0.0
2709 }
2710 }
2711 fn fafnir_evolve(&self, pointer: [f32; 2], bounds: Rect, max_scale: f32) -> f32 {
2713 let prox = self.mani_glow_intensity(pointer, bounds, 120.0);
2714 1.0 + (max_scale - 1.0) * prox
2715 }
2716 fn set_sdf_shape(&mut self, _shape: crate::layout::SdfShape) {}
2718
2719 fn enter_portal(&mut self, _z_index: i32) {}
2729
2730 fn exit_portal(&mut self) {}
2734
2735 fn viewport_size(&self) -> Rect {
2738 Rect::new(0.0, 0.0, 1920.0, 1080.0)
2739 }
2740
2741 fn announce(&mut self, _message: &str, _priority: AnnouncementPriority) {}
2747}
2748
2749pub mod accessibility {
2751 pub fn relative_luminance(color: [f32; 4]) -> f32 {
2753 let f = |c: f32| {
2754 if c <= 0.03928 {
2755 c / 12.92
2756 } else {
2757 ((c + 0.055) / 1.055).powf(2.4)
2758 }
2759 };
2760 0.2126 * f(color[0]) + 0.7152 * f(color[1]) + 0.0722 * f(color[2])
2761 }
2762
2763 pub fn contrast_ratio(c1: [f32; 4], c2: [f32; 4]) -> f32 {
2765 let l1 = relative_luminance(c1);
2766 let l2 = relative_luminance(c2);
2767 let (light, dark) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
2768 (light + 0.05) / (dark + 0.05)
2769 }
2770}
2771#[derive(
2773 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
2774)]
2775pub enum RenderTier {
2776 Tier1GPU = 0,
2778 Tier2GPU = 1,
2780 Tier3Fallback = 2,
2782}
2783use bytemuck::{Pod, Zeroable};
2787#[repr(C)]
2789#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2790pub struct ColorTheme {
2791 pub primary_neon: [f32; 4], pub shatter_neon: [f32; 4],
2793 pub glass_base: [f32; 4],
2794 pub glass_edge: [f32; 4],
2795 pub rune_glow: [f32; 4],
2796 pub ember_core: [f32; 4],
2797 pub background_deep: [f32; 4],
2798 pub mani_glow: [f32; 4], pub glass_blur_strength: f32,
2800 pub shatter_edge_width: f32,
2801 pub neon_bloom_radius: f32,
2802 pub rune_opacity: f32,
2803 pub glass_tint_adapt: f32,
2806 pub glass_ior: f32,
2808 pub color_space: u32,
2810 pub _pad0: f32,
2812 pub _pad1: f32,
2813 pub _pad2: f32,
2814 pub _pad3: f32,
2815 pub _pad4: f32,
2816}
2817const _: () = assert!(
2821 std::mem::size_of::<ColorTheme>() == 176,
2822 "ColorTheme Rust/WGSL layout mismatch: expected 176 bytes"
2823);
2824impl ColorTheme {
2825 pub fn asgard() -> Self {
2827 Self {
2828 primary_neon: [0.0, 1.0, 0.95, 1.2],
2829 shatter_neon: [1.0, 0.0, 0.75, 1.5],
2830 glass_base: [0.04, 0.04, 0.06, 0.82],
2831 glass_edge: [0.0, 0.45, 0.55, 0.6],
2832 rune_glow: [0.75, 0.98, 1.0, 0.9],
2833 ember_core: [0.95, 0.12, 0.12, 1.0],
2834 background_deep: [0.01, 0.01, 0.03, 1.0],
2835 mani_glow: [0.7, 0.9, 1.0, 0.05],
2836 glass_blur_strength: 0.6,
2837 shatter_edge_width: 1.8,
2838 neon_bloom_radius: 0.022,
2839 rune_opacity: 0.55,
2840 glass_tint_adapt: 0.35,
2841 glass_ior: 1.45,
2842 color_space: 0,
2843 _pad0: 0.0,
2844 _pad1: 0.0,
2845 _pad2: 0.0,
2846 _pad3: 0.0,
2847 _pad4: 0.0,
2848 }
2849 }
2850
2851 pub fn midgard() -> Self {
2853 Self {
2854 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],
2860 background_deep: [0.05, 0.05, 0.07, 1.0],
2861 mani_glow: [0.0, 0.0, 0.0, 0.0], glass_blur_strength: 0.0, shatter_edge_width: 1.0,
2864 neon_bloom_radius: 0.0,
2865 rune_opacity: 0.0,
2866 glass_tint_adapt: 0.0,
2867 glass_ior: 1.0,
2868 color_space: 0,
2869 _pad0: 0.0,
2870 _pad1: 0.0,
2871 _pad2: 0.0,
2872 _pad3: 0.0,
2873 _pad4: 0.0,
2874 }
2875 }
2876
2877 pub fn cyberpunk_viking() -> Self {
2878 Self::asgard()
2879 }
2880 pub fn vibrant_glass() -> Self {
2881 Self {
2882 primary_neon: [0.0, 1.0, 0.95, 1.2],
2883 shatter_neon: [1.0, 0.0, 0.75, 1.5],
2884 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],
2887 ember_core: [1.0, 0.4, 0.1, 1.0],
2888 background_deep: [0.05, 0.05, 0.1, 1.0],
2889 mani_glow: [0.7, 0.9, 1.0, 0.05],
2890 glass_blur_strength: 0.9,
2891 shatter_edge_width: 1.8,
2892 neon_bloom_radius: 0.022,
2893 rune_opacity: 0.55,
2894 glass_tint_adapt: 0.65,
2895 glass_ior: 1.45,
2896 color_space: 0,
2897 _pad0: 0.0,
2898 _pad1: 0.0,
2899 _pad2: 0.0,
2900 _pad3: 0.0,
2901 _pad4: 0.0,
2902 }
2903 }
2904
2905 pub fn berserker() -> Self {
2907 Self {
2908 primary_neon: [1.0, 0.08, 0.12, 1.8],
2909 shatter_neon: [0.95, 0.92, 0.88, 1.6],
2910 glass_base: [0.03, 0.02, 0.02, 0.88],
2911 glass_edge: [0.8, 0.35, 0.08, 0.7],
2912 rune_glow: [0.9, 0.72, 0.3, 1.0],
2913 ember_core: [0.98, 0.25, 0.05, 1.0],
2914 background_deep: [0.01, 0.005, 0.005, 1.0],
2915 mani_glow: [0.8, 0.2, 0.05, 0.08],
2916 glass_blur_strength: 0.85,
2917 shatter_edge_width: 2.8,
2918 neon_bloom_radius: 0.035,
2919 rune_opacity: 0.85,
2920 glass_tint_adapt: 0.15,
2921 glass_ior: 1.85,
2922 color_space: 0,
2923 _pad0: 0.0,
2924 _pad1: 0.0,
2925 _pad2: 0.0,
2926 _pad3: 0.0,
2927 _pad4: 0.0,
2928 }
2929 }
2930}
2931impl Default for ColorTheme {
2932 fn default() -> Self {
2933 Self::vibrant_glass()
2934 }
2935}
2936#[repr(C)]
2938#[derive(Copy, Clone, Debug, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
2939pub struct SceneUniforms {
2940 pub view: glam::Mat4,
2941 pub proj: glam::Mat4,
2942 pub time: f32,
2943 pub delta_time: f32,
2944 pub resolution: [f32; 2],
2945 pub mouse: [f32; 2],
2946 pub mouse_velocity: [f32; 2],
2947 pub shatter_origin: [f32; 2],
2948 pub shatter_time: f32,
2949 pub shatter_force: f32,
2950 pub berzerker_rage: f32,
2951 pub berzerker_mode: u32,
2952 pub scroll_offset: f32,
2953 pub scale_factor: f32,
2954 pub scene_type: u32,
2955 pub _pad_vec2_align: [u32; 1], pub fireball_pos: [f32; 2],
2957 pub _pad: [f32; 4], }
2959
2960pub const SCENE_AURORA: u32 = 0;
2961pub const SCENE_VOID: u32 = 1;
2962pub const SCENE_NEBULA: u32 = 2;
2963pub const SCENE_GLITCH: u32 = 3;
2964pub const SCENE_YGGDRASIL: u32 = 4;
2965
2966pub fn resolve_scene_by_name(name: &str) -> Option<u32> {
2971 let normalized = name.to_lowercase().replace(['-', '_', ' ', '.'], "");
2972 match normalized.as_str() {
2973 "aurora" => Some(SCENE_AURORA),
2974 "void" | "empty" | "none" | "blank" => Some(SCENE_VOID),
2975 "nebula" => Some(SCENE_NEBULA),
2976 "glitch" => Some(SCENE_GLITCH),
2977 "yggdrasil" | "worldtree" | "tree" => Some(SCENE_YGGDRASIL),
2978 _ => None,
2979 }
2980}
2981
2982impl SceneUniforms {
2983 pub fn new(width: f32, height: f32) -> Self {
2984 Self {
2985 view: glam::Mat4::IDENTITY,
2986 proj: glam::Mat4::orthographic_lh(0.0, width, height, 0.0, -100.0, 100.0),
2987 time: 0.0,
2988 delta_time: 0.016,
2989 resolution: [width, height],
2990 mouse: [0.5, 0.5],
2991 mouse_velocity: [0.0, 0.0],
2992 shatter_origin: [0.5, 0.5],
2993 shatter_time: -100.0,
2994 shatter_force: 0.0,
2995 berzerker_rage: 0.0,
2996 berzerker_mode: 0,
2997 scroll_offset: 0.0,
2998 scale_factor: 1.0,
2999 scene_type: SCENE_AURORA,
3000 _pad_vec2_align: [0],
3001 fireball_pos: [0.0, 0.0],
3002 _pad: [0.0; 4],
3003 }
3004 }
3005}
3006#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
3008pub struct Mesh {
3009 pub vertices: Vec<[f32; 3]>,
3010 pub normals: Vec<[f32; 3]>,
3011 pub indices: Vec<u32>,
3012}
3013impl Mesh {
3014 pub fn from_obj(data: &[u8]) -> anyhow::Result<Vec<Self>> {
3015 let mut cursor = std::io::Cursor::new(data);
3016 let (models, _) = tobj::load_obj_buf(&mut cursor, &tobj::LoadOptions::default(), |_| {
3017 Ok((Vec::new(), Default::default()))
3018 })?;
3019 let mut meshes = Vec::new();
3020 for m in models {
3021 let mesh = m.mesh;
3022 let vertices: Vec<[f32; 3]> = mesh
3023 .positions
3024 .chunks(3)
3025 .map(|c| [c[0], c[1], c[2]])
3026 .collect();
3027 let normals = if mesh.normals.is_empty() {
3028 vec![[0.0, 0.0, 1.0]; vertices.len()]
3029 } else {
3030 mesh.normals.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
3031 };
3032 meshes.push(Mesh {
3033 vertices,
3034 normals,
3035 indices: mesh.indices,
3036 });
3037 }
3038 Ok(meshes)
3039 }
3040 pub fn from_stl(data: &[u8]) -> anyhow::Result<Self> {
3041 let mut cursor = std::io::Cursor::new(data);
3042 let stl = stl_io::read_stl(&mut cursor)?;
3043 let vertices: Vec<[f32; 3]> = stl.vertices.iter().map(|v| [v[0], v[1], v[2]]).collect();
3044 let mut indices = Vec::new();
3045 for face in stl.faces {
3046 indices.push(face.vertices[0] as u32);
3047 indices.push(face.vertices[1] as u32);
3048 indices.push(face.vertices[2] as u32);
3049 }
3050 let normals = vec![[0.0, 0.0, 1.0]; vertices.len()];
3051 Ok(Mesh {
3052 vertices,
3053 normals,
3054 indices,
3055 })
3056 }
3057}
3058
3059#[derive(Debug, Clone, Copy, PartialEq)]
3065pub struct Transform3D {
3066 pub position: glam::Vec3,
3067 pub rotation: glam::Quat,
3068 pub scale: glam::Vec3,
3069}
3070
3071impl Default for Transform3D {
3072 fn default() -> Self {
3073 Self {
3074 position: glam::Vec3::ZERO,
3075 rotation: glam::Quat::IDENTITY,
3076 scale: glam::Vec3::ONE,
3077 }
3078 }
3079}
3080
3081impl Transform3D {
3082 pub fn to_matrix(&self) -> glam::Mat4 {
3084 glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
3085 }
3086
3087 pub fn from_2d(x: f32, y: f32, rotation: f32) -> Self {
3089 Self {
3090 position: glam::Vec3::new(x, y, 0.0),
3091 rotation: glam::Quat::from_rotation_z(rotation),
3092 scale: glam::Vec3::ONE,
3093 }
3094 }
3095}
3096
3097#[derive(Debug, Clone, Copy)]
3099pub struct Camera3D {
3100 pub position: glam::Vec3,
3102 pub target: glam::Vec3,
3104 pub up: glam::Vec3,
3106 pub fov_y: f32,
3108 pub near: f32,
3110 pub far: f32,
3112 pub perspective: bool,
3114 pub aspect: f32,
3116}
3117
3118#[derive(Debug, Clone, Copy, PartialEq)]
3120pub struct Material3D {
3121 pub base_color: [f32; 4],
3123 pub metallic: f32,
3125 pub roughness: f32,
3127 pub emissive: [f32; 3],
3129 pub opacity: f32,
3131}
3132
3133impl Default for Material3D {
3134 fn default() -> Self {
3135 Self {
3136 base_color: [1.0, 1.0, 1.0, 1.0],
3137 metallic: 0.0,
3138 roughness: 0.5,
3139 emissive: [0.0, 0.0, 0.0],
3140 opacity: 1.0,
3141 }
3142 }
3143}
3144
3145impl Material3D {
3146 pub fn unlit(color: [f32; 4]) -> Self {
3148 Self {
3149 base_color: color,
3150 metallic: 0.0,
3151 roughness: 1.0,
3152 emissive: [0.0, 0.0, 0.0],
3153 opacity: color[3],
3154 }
3155 }
3156
3157 pub fn metallic(color: [f32; 4], roughness: f32) -> Self {
3159 Self {
3160 base_color: color,
3161 metallic: 1.0,
3162 roughness: roughness.clamp(0.0, 1.0),
3163 emissive: [0.0, 0.0, 0.0],
3164 opacity: color[3],
3165 }
3166 }
3167}
3168
3169impl Default for Camera3D {
3170 fn default() -> Self {
3171 Self {
3172 position: glam::Vec3::new(0.0, 0.0, 10.0),
3173 target: glam::Vec3::ZERO,
3174 up: glam::Vec3::Y,
3175 fov_y: 45.0f32.to_radians(),
3176 near: 0.1,
3177 far: 1000.0,
3178 perspective: true,
3179 aspect: 16.0 / 9.0,
3180 }
3181 }
3182}
3183
3184impl Camera3D {
3185 pub fn view_matrix(&self) -> glam::Mat4 {
3187 glam::Mat4::look_at_lh(self.position, self.target, self.up)
3188 }
3189
3190 pub fn projection_matrix(&self) -> glam::Mat4 {
3192 if self.perspective {
3193 glam::Mat4::perspective_lh(self.fov_y, self.aspect, self.near, self.far)
3194 } else {
3195 let top = self.fov_y;
3197 let right = top * self.aspect;
3198 glam::Mat4::orthographic_lh(-right, right, -top, top, self.near, self.far)
3199 }
3200 }
3201
3202 pub fn view_projection(&self) -> glam::Mat4 {
3204 self.projection_matrix() * self.view_matrix()
3205 }
3206}
3207
3208pub trait FrameRenderer<E = ()>: Renderer {
3211 fn begin_frame(&mut self) -> E;
3212 fn render_frame(&mut self) {
3213 }
3215 fn end_frame(&mut self, encoder: E);
3216}
3217use std::sync::Arc;
3218type SubscriberList<T> = Arc<std::sync::Mutex<Vec<Box<dyn Fn(&T) + Send + Sync>>>>;
3219
3220fn invoke_subscribers_safely<T>(subs: &SubscriberList<T>, val: &T) -> usize
3226where
3227 {
3231 let guard = match subs.lock() {
3237 Ok(g) => g,
3238 Err(poisoned) => {
3239 log::warn!(
3240 "[State] subscriber list mutex was poisoned; recovering"
3241 );
3242 poisoned.into_inner()
3243 }
3244 };
3245 let mut invoked = 0usize;
3246 for cb in guard.iter() {
3247 let cb_ref: &(dyn Fn(&T) + Send + Sync) = &**cb;
3251 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3255 cb_ref(val);
3256 }));
3257 if let Err(payload) = result {
3258 let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
3259 (*s).to_string()
3260 } else if let Some(s) = payload.downcast_ref::<String>() {
3261 s.clone()
3262 } else {
3263 "unknown panic payload".to_string()
3264 };
3265 log::error!("[State] subscriber callback panicked: {msg}");
3266 } else {
3268 invoked += 1;
3269 }
3270 }
3271 invoked
3272}
3273#[derive(Clone)]
3293pub struct State<T: Clone + Send + Sync + 'static> {
3294 swap: Arc<arc_swap::ArcSwap<T>>,
3295 metadata_swap: Arc<arc_swap::ArcSwap<Option<agents::MutationMetadata>>>,
3296 #[cfg(not(target_arch = "wasm32"))]
3297 tvar: Arc<stm::TVar<T>>,
3298 #[cfg(not(target_arch = "wasm32"))]
3299 metadata_tvar: Arc<stm::TVar<Option<agents::MutationMetadata>>>,
3300 subscribers: SubscriberList<T>,
3301 version: Arc<std::sync::atomic::AtomicU64>,
3302 resolution: agents::ConflictResolution,
3303}
3304impl<T: Clone + Send + Sync + 'static> State<T> {
3305 pub fn new(value: T) -> Self {
3307 #[cfg(not(target_arch = "wasm32"))]
3308 let tvar = Arc::new(stm::TVar::new(value.clone()));
3309 #[cfg(not(target_arch = "wasm32"))]
3310 let metadata_tvar = Arc::new(stm::TVar::new(None));
3311 Self {
3312 swap: Arc::new(arc_swap::ArcSwap::from_pointee(value)),
3313 metadata_swap: Arc::new(arc_swap::ArcSwap::new(Arc::new(None))),
3314 #[cfg(not(target_arch = "wasm32"))]
3315 tvar,
3316 #[cfg(not(target_arch = "wasm32"))]
3317 metadata_tvar,
3318 subscribers: Arc::new(std::sync::Mutex::new(Vec::new())),
3319 version: Arc::new(std::sync::atomic::AtomicU64::new(0)),
3320 resolution: agents::ConflictResolution::default(),
3321 }
3322 }
3323 pub fn with_resolution(mut self, resolution: agents::ConflictResolution) -> Self {
3325 self.resolution = resolution;
3326 self
3327 }
3328 pub fn get(&self) -> T {
3330 (**self.swap.load()).clone()
3331 }
3332 pub fn set(&self, value: T) {
3334 #[cfg(not(target_arch = "wasm32"))]
3335 let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3336 let new_meta = agents::get_current_mutation_metadata();
3337 let existing_meta = self.metadata_tvar.read(tx)?;
3338 let mut skip = false;
3339 if self.resolution == agents::ConflictResolution::PriorityWins
3340 && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3341 && new_m.priority < old_m.priority
3342 {
3343 skip = true;
3344 }
3345 if !skip {
3346 self.tvar.write(tx, value.clone())?;
3347 self.metadata_tvar.write(tx, new_meta)?;
3348 Ok((false, value.clone(), new_meta))
3349 } else {
3350 Ok((true, self.tvar.read(tx)?, existing_meta))
3351 }
3352 });
3353 #[cfg(target_arch = "wasm32")]
3354 let (was_skipped, final_val, final_meta) =
3355 (false, value, agents::get_current_mutation_metadata());
3356 if was_skipped {
3357 if let (Some(new_m), Some(old_m)) =
3358 (agents::get_current_mutation_metadata(), final_meta)
3359 {
3360 agents::notify_conflict(agents::ConflictEvent {
3361 agent_id: new_m.agent_id,
3362 priority: new_m.priority,
3363 existing_agent_id: old_m.agent_id,
3364 existing_priority: old_m.priority,
3365 timestamp_ms: new_m.timestamp_ms,
3366 });
3367 }
3368 return;
3369 }
3370 self.swap.store(Arc::new(final_val.clone()));
3371 self.metadata_swap.store(Arc::new(final_meta));
3372 self.version
3373 .fetch_add(1, std::sync::atomic::Ordering::Release);
3374 let subs = Arc::clone(&self.subscribers);
3375 if crate::is_batching() {
3376 crate::enqueue_batch_task(Box::new(move || {
3377 let _ = invoke_subscribers_safely(&subs, &final_val);
3378 }));
3379 } else {
3380 let _ = invoke_subscribers_safely(&subs, &final_val);
3381 }
3382 }
3383
3384 pub fn set_direct(&self, value: T) {
3398 self.swap.store(Arc::new(value.clone()));
3399 let new_meta = agents::get_current_mutation_metadata();
3400 self.metadata_swap.store(Arc::new(new_meta));
3401 self.version
3402 .fetch_add(1, std::sync::atomic::Ordering::Release);
3403 #[cfg(not(target_arch = "wasm32"))]
3404 {
3405 let _ = stm::atomically(|tx| {
3406 self.tvar.write(tx, value.clone())?;
3407 let meta = agents::get_current_mutation_metadata();
3408 self.metadata_tvar.write(tx, meta)?;
3409 Ok(())
3410 });
3411 }
3412 let subs = Arc::clone(&self.subscribers);
3413 if crate::is_batching() {
3414 crate::enqueue_batch_task(Box::new(move || {
3415 let _ = invoke_subscribers_safely(&subs, &value);
3416 }));
3417 } else {
3418 let _ = invoke_subscribers_safely(&subs, &value);
3419 }
3420 }
3421 pub fn mutate<F: Fn(&T) -> T>(&self, f: F) {
3422 #[cfg(not(target_arch = "wasm32"))]
3423 {
3424 let (was_skipped, final_val, final_meta) = stm::atomically(|tx| {
3425 let new_meta = agents::get_current_mutation_metadata();
3426 let existing_meta = self.metadata_tvar.read(tx)?;
3427 let mut skip = false;
3428 if self.resolution == agents::ConflictResolution::PriorityWins
3429 && let (Some(new_m), Some(old_m)) = (new_meta, existing_meta)
3430 && new_m.priority < old_m.priority
3431 {
3432 skip = true;
3433 }
3434 if !skip {
3435 let current = self.tvar.read(tx)?;
3436 let next = f(¤t);
3437 self.tvar.write(tx, next.clone())?;
3438 self.metadata_tvar.write(tx, new_meta)?;
3439 Ok((false, next, new_meta))
3440 } else {
3441 Ok((true, self.tvar.read(tx)?, existing_meta))
3442 }
3443 });
3444 if was_skipped {
3445 if let (Some(new_m), Some(old_m)) =
3446 (agents::get_current_mutation_metadata(), final_meta)
3447 {
3448 agents::notify_conflict(agents::ConflictEvent {
3449 agent_id: new_m.agent_id,
3450 priority: new_m.priority,
3451 existing_agent_id: old_m.agent_id,
3452 existing_priority: old_m.priority,
3453 timestamp_ms: new_m.timestamp_ms,
3454 });
3455 }
3456 return;
3457 }
3458 self.swap.store(Arc::new(final_val.clone()));
3459 self.metadata_swap.store(Arc::new(final_meta));
3460 self.version
3461 .fetch_add(1, std::sync::atomic::Ordering::Release);
3462 let subs = Arc::clone(&self.subscribers);
3463 if crate::is_batching() {
3464 crate::enqueue_batch_task(Box::new(move || {
3465 let _ = invoke_subscribers_safely(&subs, &final_val);
3466 }));
3467 } else {
3468 let _ = invoke_subscribers_safely(&subs, &final_val);
3469 }
3470 }
3471 #[cfg(target_arch = "wasm32")]
3472 {
3473 self.set(f(&self.get()));
3474 }
3475 }
3476 pub fn version(&self) -> u64 {
3478 self.version.load(std::sync::atomic::Ordering::Acquire)
3479 }
3480 pub fn subscribe<F: Fn(&T) + Send + Sync + 'static>(&self, callback: F) {
3482 self.subscribers.lock().unwrap().push(Box::new(callback));
3483 }
3484}
3485use crate::runtime::NodeStateSnapshot;
3486use std::sync::OnceLock;
3487use std::sync::atomic::{AtomicBool, Ordering};
3488
3489#[cfg(not(target_arch = "wasm32"))]
3505static FALLBACK_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
3506
3507#[cfg(not(target_arch = "wasm32"))]
3510static FALLBACK_WORKER_COUNT: OnceLock<usize> = OnceLock::new();
3511
3512#[cfg(not(target_arch = "wasm32"))]
3513fn fallback_runtime() -> &'static tokio::runtime::Runtime {
3514 FALLBACK_RUNTIME.get_or_init(|| {
3515 let worker_count = *FALLBACK_WORKER_COUNT.get_or_init(|| {
3519 let available = std::thread::available_parallelism()
3520 .map(|n| n.get())
3521 .unwrap_or(2);
3522 available.saturating_sub(1).clamp(1, 8)
3523 });
3524 tokio::runtime::Builder::new_current_thread()
3525 .worker_threads(worker_count)
3526 .thread_name("cvkg-fallback-rt")
3527 .enable_all()
3528 .build()
3529 .expect("failed to build fallback tokio runtime")
3530 })
3531}
3532pub static SYSTEM_STATE: OnceLock<Arc<arc_swap::ArcSwap<KnowledgeState>>> = OnceLock::new();
3534#[cfg(not(target_arch = "wasm32"))]
3535static KNOWLEDGE_TVAR: OnceLock<stm::TVar<KnowledgeState>> = OnceLock::new();
3536static IS_BATCHING: AtomicBool = AtomicBool::new(false);
3537pub static IS_RENDERING: AtomicBool = AtomicBool::new(false);
3538pub static LAYOUT_DIRTY: AtomicBool = AtomicBool::new(false);
3539type BatchQueue = OnceLock<std::sync::Mutex<Vec<Box<dyn FnOnce() + Send + Sync>>>>;
3540static BATCH_QUEUE: BatchQueue = OnceLock::new();
3541static STATE_WRITE_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3544pub fn is_batching() -> bool {
3546 IS_BATCHING.load(Ordering::Acquire)
3547}
3548pub fn is_rendering() -> bool {
3550 IS_RENDERING.load(Ordering::Acquire)
3551}
3552pub fn begin_render_phase() {
3554 IS_RENDERING.store(true, Ordering::Release);
3555}
3556pub fn end_render_phase() {
3558 IS_RENDERING.store(false, Ordering::Release);
3559}
3560pub fn enqueue_batch_task(task: Box<dyn FnOnce() + Send + Sync>) {
3562 let mut queue = BATCH_QUEUE
3563 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3564 .lock()
3565 .unwrap();
3566 queue.push(task);
3567}
3568pub fn batch<F: FnOnce()>(f: F) {
3572 if IS_BATCHING.swap(true, Ordering::AcqRel) {
3573 f();
3575 return;
3576 }
3577 f();
3578 IS_BATCHING.store(false, Ordering::Release);
3579 let mut queue = BATCH_QUEUE
3580 .get_or_init(|| std::sync::Mutex::new(Vec::new()))
3581 .lock()
3582 .unwrap();
3583 let tasks: Vec<_> = queue.drain(..).collect();
3584 drop(queue);
3585 for task in tasks {
3586 task();
3587 }
3588}
3589pub fn get_system_state() -> Arc<arc_swap::ArcSwap<KnowledgeState>> {
3591 SYSTEM_STATE
3592 .get_or_init(|| Arc::new(arc_swap::ArcSwap::from_pointee(KnowledgeState::default())))
3593 .clone()
3594}
3595pub fn load_system_state() -> arc_swap::Guard<Arc<KnowledgeState>> {
3596 get_system_state().load()
3597}
3598pub fn update_system_state<F>(f: F)
3599where
3600 F: FnOnce(&KnowledgeState) -> KnowledgeState,
3601{
3602 let _lock = STATE_WRITE_MUTEX.lock().unwrap();
3603 if is_rendering() {
3604 log::warn!(
3605 "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3606 );
3607 }
3608 LAYOUT_DIRTY.store(true, Ordering::SeqCst);
3609 let swap = get_system_state();
3610 let current = swap.load();
3611 let new_state = Arc::new(f(¤t));
3612 swap.store(Arc::clone(&new_state));
3613 #[cfg(not(target_arch = "wasm32"))]
3614 {
3615 let tvar = KNOWLEDGE_TVAR.get_or_init(|| stm::TVar::new((*new_state).clone()));
3616 stm::atomically(|tx| tvar.write(tx, (*new_state).clone()));
3617 }
3618}
3619pub fn transact_system_state<F>(f: F)
3620where
3621 F: Fn(&KnowledgeState) -> KnowledgeState,
3622{
3623 let _lock = STATE_WRITE_MUTEX.lock().unwrap();
3624 #[cfg(not(target_arch = "wasm32"))]
3625 {
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 let tvar = KNOWLEDGE_TVAR
3632 .get_or_init(|| stm::TVar::new((**get_system_state().load()).clone()))
3633 .clone();
3634 let new_state = stm::atomically(move |tx| {
3635 let current = tvar.read(tx)?;
3636 let next = f(¤t);
3637 tvar.write(tx, next.clone())?;
3638 Ok(next)
3639 });
3640 get_system_state().store(Arc::new(new_state));
3641 }
3642 #[cfg(target_arch = "wasm32")]
3643 {
3644 if is_rendering() {
3645 log::warn!(
3646 "LAYOUT THRASH DETECTED: System state mutated during render phase. This may trigger redundant layout passes and impact performance."
3647 );
3648 }
3649 update_system_state(f);
3650 }
3651}
3652impl KnowledgeState {
3653 pub fn new() -> Self {
3655 Self::default()
3656 }
3657 pub fn set_component_state<T: 'static + Send + Sync>(&mut self, id: u64, state: T) {
3659 self.component_states
3660 .insert(id, Arc::new(std::sync::RwLock::new(state)));
3661 }
3662 pub fn get_component_state<T: 'static + Send + Sync>(
3664 &self,
3665 id: u64,
3666 ) -> Option<Arc<std::sync::RwLock<T>>> {
3667 let stored = self.component_states.get(&id)?;
3668 let any_ref = stored.read().ok()?;
3672 let _verified: &T = any_ref.downcast_ref::<T>()?;
3674 drop(any_ref);
3675 let raw = Arc::into_raw(stored.clone());
3678 Some(unsafe { Arc::from_raw(raw as *const std::sync::RwLock<T>) })
3679 }
3680 pub fn remember(&mut self, fragment: KnowledgeFragment) {
3682 self.fragments.insert(fragment.id.clone(), fragment);
3683 }
3684 pub fn process_query(&mut self, query: &str) {
3686 let query_lower = query.to_lowercase();
3687 let mut results: Vec<(f32, String)> = self
3688 .fragments
3689 .iter()
3690 .map(|(id, frag)| {
3691 let mut score = 0.0;
3692 if frag.summary.to_lowercase().contains(&query_lower) {
3693 score += 1.0;
3694 }
3695 if frag.source.to_lowercase().contains(&query_lower) {
3696 score += 0.5;
3697 }
3698 (score, id.clone())
3699 })
3700 .filter(|(score, _)| *score > 0.0)
3701 .collect();
3702 results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
3704 self.last_query_results = results.into_iter().map(|(_, id)| id).take(5).collect();
3705 }
3706 pub fn snapshot(&self) -> Vec<NodeStateSnapshot> {
3708 let mut snapshots = Vec::new();
3709 for frag in self.fragments.values() {
3711 if let Ok(val) = serde_json::to_value(frag) {
3712 snapshots.push(NodeStateSnapshot { id: 0, state: val });
3713 }
3714 }
3715 snapshots
3716 }
3717}
3718#[derive(Clone)]
3720pub struct Binding<T: Clone + Send + Sync + 'static> {
3721 swap: Arc<arc_swap::ArcSwap<T>>,
3722 #[cfg(not(target_arch = "wasm32"))]
3723 tvar: Arc<stm::TVar<T>>,
3724 version: Arc<std::sync::atomic::AtomicU64>,
3725}
3726impl<T: Clone + Send + Sync + 'static> Binding<T> {
3727 pub fn from_state(state: &State<T>) -> Self {
3729 Self {
3730 swap: Arc::clone(&state.swap),
3731 #[cfg(not(target_arch = "wasm32"))]
3732 tvar: Arc::clone(&state.tvar),
3733 version: Arc::clone(&state.version),
3734 }
3735 }
3736 pub fn get(&self) -> T {
3738 (**self.swap.load()).clone()
3739 }
3740 pub fn set(&self, value: T) {
3742 self.swap.store(Arc::new(value.clone()));
3743 #[cfg(not(target_arch = "wasm32"))]
3744 {
3745 let tvar = Arc::clone(&self.tvar);
3746 let v = value.clone();
3747 stm::atomically(move |tx| tvar.write(tx, v.clone()));
3748 }
3749 self.version
3750 .fetch_add(1, std::sync::atomic::Ordering::Release);
3751 }
3752 pub fn version(&self) -> u64 {
3754 self.version.load(std::sync::atomic::Ordering::Acquire)
3755 }
3756}
3757#[cfg(not(target_arch = "wasm32"))]
3758pub fn transact_pair<A, B, F>(state_a: &State<A>, state_b: &State<B>, f: F)
3759where
3760 A: Clone + Send + Sync + 'static,
3761 B: Clone + Send + Sync + 'static,
3762 F: Fn(&A, &B) -> (A, B),
3763{
3764 let tvar_a = Arc::clone(&state_a.tvar);
3765 let tvar_b = Arc::clone(&state_b.tvar);
3766 let (new_a, new_b) = stm::atomically(move |tx| {
3767 let a = tvar_a.read(tx)?;
3768 let b = tvar_b.read(tx)?;
3769 let (na, nb) = f(&a, &b);
3770 tvar_a.write(tx, na.clone())?;
3771 tvar_b.write(tx, nb.clone())?;
3772 Ok((na, nb))
3773 });
3774 state_a.swap.store(Arc::new(new_a.clone()));
3775 state_b.swap.store(Arc::new(new_b.clone()));
3776 state_a
3777 .version
3778 .fetch_add(1, std::sync::atomic::Ordering::Release);
3779 state_b
3780 .version
3781 .fetch_add(1, std::sync::atomic::Ordering::Release);
3782 let subs_a = Arc::clone(&state_a.subscribers);
3783 let subs_b = Arc::clone(&state_b.subscribers);
3784 if crate::is_batching() {
3785 crate::enqueue_batch_task(Box::new(move || {
3786 {
3787 let s = subs_a.lock().unwrap();
3788 for cb in s.iter() {
3789 cb(&new_a);
3790 }
3791 }
3792 {
3793 let s = subs_b.lock().unwrap();
3794 for cb in s.iter() {
3795 cb(&new_b);
3796 }
3797 }
3798 }));
3799 } else {
3800 {
3801 let s = subs_a.lock().unwrap();
3802 for cb in s.iter() {
3803 cb(&new_a);
3804 }
3805 }
3806 {
3807 let s = subs_b.lock().unwrap();
3808 for cb in s.iter() {
3809 cb(&new_b);
3810 }
3811 }
3812 }
3813}
3814use std::any::TypeId;
3815use std::sync::Mutex;
3816pub(crate) static ENVIRONMENT: OnceLock<
3818 Mutex<HashMap<TypeId, Box<dyn std::any::Any + Send + Sync>>>,
3819> = OnceLock::new();
3820pub trait EnvKey: 'static + Send + Sync {
3823 type Value: Clone + Send + Sync + 'static;
3825 fn default_value() -> Self::Value;
3827}
3828pub struct YggdrasilKey;
3830impl EnvKey for YggdrasilKey {
3831 type Value = YggdrasilTokens;
3832 fn default_value() -> Self::Value {
3833 default_tokens()
3834 }
3835}
3836#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3839pub enum Appearance {
3840 Light,
3841 Dark,
3842}
3843#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3845pub enum Orientation {
3846 Horizontal,
3847 Vertical,
3848}
3849#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3851pub struct GridPlacement {
3852 pub column: i32,
3854 pub column_span: u32,
3856 pub row: i32,
3858 pub row_span: u32,
3860}
3861#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3863pub enum Alignment {
3864 #[default]
3865 Center,
3866 Leading,
3867 Trailing,
3868 Top,
3869 Bottom,
3870}
3871#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
3873pub enum Distribution {
3874 #[default]
3875 Fill,
3876 Center,
3877 Leading,
3878 Trailing,
3879 SpaceBetween,
3880 SpaceAround,
3881 SpaceEvenly,
3882}
3883#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3885pub struct Color {
3886 pub r: f32,
3887 pub g: f32,
3888 pub b: f32,
3889 pub a: f32,
3890}
3891impl Color {
3892 pub const BLACK: Color = Color {
3893 r: 0.0,
3894 g: 0.0,
3895 b: 0.0,
3896 a: 1.0,
3897 };
3898 pub const WHITE: Color = Color {
3899 r: 1.0,
3900 g: 1.0,
3901 b: 1.0,
3902 a: 1.0,
3903 };
3904 pub const TRANSPARENT: Color = Color {
3905 r: 0.0,
3906 g: 0.0,
3907 b: 0.0,
3908 a: 0.0,
3909 };
3910 pub const RED: Color = Color {
3911 r: 1.0,
3912 g: 0.0,
3913 b: 0.0,
3914 a: 1.0,
3915 };
3916 pub const GREEN: Color = Color {
3917 r: 0.0,
3918 g: 1.0,
3919 b: 0.0,
3920 a: 1.0,
3921 };
3922 pub const BLUE: Color = Color {
3923 r: 0.0,
3924 g: 0.0,
3925 b: 1.0,
3926 a: 1.0,
3927 };
3928 pub const VIKING_GOLD: Color = Color {
3929 r: 1.0,
3930 g: 0.84,
3931 b: 0.0,
3932 a: 1.0,
3933 };
3934 pub const MAGENTA_LIQUID: Color = Color {
3935 r: 1.0,
3936 g: 0.0,
3937 b: 1.0,
3938 a: 1.0,
3939 };
3940 pub const TACTICAL_OBSIDIAN: Color = Color {
3941 r: 0.05,
3942 g: 0.05,
3943 b: 0.07,
3944 a: 1.0,
3945 };
3946 pub fn relative_luminance(&self) -> f32 {
3948 fn res(c: f32) -> f32 {
3949 if c <= 0.03928 {
3950 c / 12.92
3951 } else {
3952 ((c + 0.055) / 1.055).powf(2.4)
3953 }
3954 }
3955 0.2126 * res(self.r) + 0.7152 * res(self.g) + 0.0722 * res(self.b)
3956 }
3957 pub fn contrast_ratio(&self, other: &Color) -> f32 {
3959 let l1 = self.relative_luminance();
3960 let l2 = other.relative_luminance();
3961 if l1 > l2 {
3962 (l1 + 0.05) / (l2 + 0.05)
3963 } else {
3964 (l2 + 0.05) / (l1 + 0.05)
3965 }
3966 }
3967 pub const CYAN: Color = Color {
3968 r: 0.0,
3969 g: 1.0,
3970 b: 1.0,
3971 a: 1.0,
3972 };
3973 pub const YELLOW: Color = Color {
3974 r: 1.0,
3975 g: 1.0,
3976 b: 0.0,
3977 a: 1.0,
3978 };
3979 pub const MAGENTA: Color = Color {
3980 r: 1.0,
3981 g: 0.0,
3982 b: 1.0,
3983 a: 1.0,
3984 };
3985 pub const GRAY: Color = Color {
3986 r: 0.5,
3987 g: 0.5,
3988 b: 0.5,
3989 a: 1.0,
3990 };
3991 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
3993 Self { r, g, b, a }
3994 }
3995 pub fn as_array(&self) -> [f32; 4] {
3997 [self.r, self.g, self.b, self.a]
3998 }
3999
4000 pub fn lighten(&self, amount: f32) -> Self {
4006 Self {
4007 r: (self.r + amount).clamp(0.0, 1.0),
4008 g: (self.g + amount).clamp(0.0, 1.0),
4009 b: (self.b + amount).clamp(0.0, 1.0),
4010 a: self.a,
4011 }
4012 }
4013
4014 pub fn darken(&self, amount: f32) -> Self {
4016 Self {
4017 r: (self.r - amount).clamp(0.0, 1.0),
4018 g: (self.g - amount).clamp(0.0, 1.0),
4019 b: (self.b - amount).clamp(0.0, 1.0),
4020 a: self.a,
4021 }
4022 }
4023}
4024impl View for Color {
4025 type Body = Never;
4026 fn body(self) -> Self::Body {
4027 unreachable!()
4030 }
4031 fn render(&self, renderer: &mut dyn Renderer, rect: Rect) {
4032 renderer.fill_rect(rect, self.as_array());
4033 }
4034}
4035pub struct AppearanceKey;
4037impl EnvKey for AppearanceKey {
4038 type Value = Appearance;
4039 fn default_value() -> Self::Value {
4040 Appearance::Dark }
4042}
4043
4044pub struct DirectionKey;
4046impl EnvKey for DirectionKey {
4047 type Value = Direction;
4048 fn default_value() -> Self::Value {
4049 Direction::LTR
4050 }
4051}
4052
4053pub struct StyleResolver;
4055impl StyleResolver {
4056 pub fn color(key: &str) -> String {
4058 let tokens = Environment::<YggdrasilKey>::new().get();
4059 let appearance = Environment::<AppearanceKey>::new().get();
4060 let is_dark = appearance == Appearance::Dark;
4061 tokens
4062 .get_color(key, is_dark)
4063 .unwrap_or_else(|| "#FF00FF".to_string()) }
4065 pub fn get<T: FromStr>(category: &str, key: &str) -> Option<T> {
4067 let tokens = Environment::<YggdrasilKey>::new().get();
4068 let appearance = Environment::<AppearanceKey>::new().get();
4069 let is_dark = appearance == Appearance::Dark;
4070 tokens.get(category, key, is_dark)
4071 }
4072 pub fn color_array(key: &str) -> [f32; 4] {
4076 let hex = Self::color(key);
4077 parse_hex_color(&hex)
4078 }
4079}
4080
4081fn parse_hex_color(hex: &str) -> [f32; 4] {
4083 let hex = hex.trim_start_matches('#');
4084 if hex.len() >= 6 {
4085 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255) as f32 / 255.0;
4086 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
4087 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255) as f32 / 255.0;
4088 let a = if hex.len() >= 8 {
4089 u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
4090 } else {
4091 1.0
4092 };
4093 [r, g, b, a]
4094 } else {
4095 [1.0, 0.0, 1.0, 1.0] }
4097}
4098
4099pub fn default_tokens() -> YggdrasilTokens {
4101 let mut tokens = YggdrasilTokens::new();
4102 tokens.color.insert(
4104 "background".to_string(),
4105 TokenValue::Adaptive {
4106 light: "#FFFFFF".to_string(), dark: "#000000".to_string(), },
4109 );
4110 tokens.color.insert(
4111 "primary".to_string(),
4112 TokenValue::Adaptive {
4113 light: "#007B8A".to_string(), dark: "#00FFFF".to_string(), },
4116 );
4117 tokens.color.insert(
4118 "secondary".to_string(),
4119 TokenValue::Adaptive {
4120 light: "#8A008A".to_string(), dark: "#FF00FF".to_string(), },
4123 );
4124 tokens.color.insert(
4125 "surface".to_string(),
4126 TokenValue::Adaptive {
4127 light: "#FFFFFF".to_string(),
4128 dark: "#121212".to_string(),
4129 },
4130 );
4131 tokens.color.insert(
4132 "text".to_string(),
4133 TokenValue::Adaptive {
4134 light: "#000000".to_string(),
4135 dark: "#FFFFFF".to_string(),
4136 },
4137 );
4138 tokens.color.insert(
4140 "surface_elevated".to_string(),
4141 TokenValue::Adaptive {
4142 light: "#FFFFFF".to_string(),
4143 dark: "#1A1A24".to_string(),
4144 },
4145 );
4146 tokens.color.insert(
4147 "surface_overlay".to_string(),
4148 TokenValue::Adaptive {
4149 light: "#FFFFFF".to_string(),
4150 dark: "#1E1E2E".to_string(),
4151 },
4152 );
4153 tokens.color.insert(
4154 "border".to_string(),
4155 TokenValue::Adaptive {
4156 light: "#D0D0D8".to_string(),
4157 dark: "#2A2A3A".to_string(),
4158 },
4159 );
4160 tokens.color.insert(
4161 "border_strong".to_string(),
4162 TokenValue::Adaptive {
4163 light: "#A0A0B0".to_string(),
4164 dark: "#3A3A50".to_string(),
4165 },
4166 );
4167 tokens.color.insert(
4168 "text_muted".to_string(),
4169 TokenValue::Adaptive {
4170 light: "#606070".to_string(),
4171 dark: "#8080A0".to_string(),
4172 },
4173 );
4174 tokens.color.insert(
4175 "text_dim".to_string(),
4176 TokenValue::Adaptive {
4177 light: "#9090A0".to_string(),
4178 dark: "#505070".to_string(),
4179 },
4180 );
4181 tokens.color.insert(
4182 "accent".to_string(),
4183 TokenValue::Adaptive {
4184 light: "#007B8A".to_string(), dark: "#00FFFF".to_string(), },
4187 );
4188 tokens.color.insert(
4189 "accent_hover".to_string(),
4190 TokenValue::Adaptive {
4191 light: "#00A0B0".to_string(), dark: "#33FFFF".to_string(), },
4194 );
4195 tokens.color.insert(
4196 "success".to_string(),
4197 TokenValue::Single {
4198 value: "#00E676".to_string(),
4199 },
4200 );
4201 tokens.color.insert(
4202 "warning".to_string(),
4203 TokenValue::Single {
4204 value: "#FFB300".to_string(),
4205 },
4206 );
4207 tokens.color.insert(
4208 "error".to_string(),
4209 TokenValue::Single {
4210 value: "#FF5252".to_string(),
4211 },
4212 );
4213 tokens.color.insert(
4214 "info".to_string(),
4215 TokenValue::Single {
4216 value: "#448AFF".to_string(),
4217 },
4218 );
4219 tokens.color.insert(
4220 "hover".to_string(),
4221 TokenValue::Adaptive {
4222 light: "#F0F0F5".to_string(),
4223 dark: "#252535".to_string(),
4224 },
4225 );
4226 tokens.color.insert(
4227 "active".to_string(),
4228 TokenValue::Adaptive {
4229 light: "#E0E0EB".to_string(),
4230 dark: "#303045".to_string(),
4231 },
4232 );
4233 tokens.color.insert(
4234 "disabled".to_string(),
4235 TokenValue::Adaptive {
4236 light: "#E8E8F0".to_string(),
4237 dark: "#1A1A28".to_string(),
4238 },
4239 );
4240 tokens.color.insert(
4241 "disabled_text".to_string(),
4242 TokenValue::Adaptive {
4243 light: "#B0B0C0".to_string(),
4244 dark: "#404060".to_string(),
4245 },
4246 );
4247 tokens.color.insert(
4248 "focus_ring".to_string(),
4249 TokenValue::Single {
4250 value: "#00FFFF".to_string(),
4251 },
4252 );
4253 tokens.color.insert(
4254 "shadow".to_string(),
4255 TokenValue::Adaptive {
4256 light: "#00000020".to_string(),
4257 dark: "#00000060".to_string(),
4258 },
4259 );
4260 tokens.color.insert(
4261 "code_bg".to_string(),
4262 TokenValue::Adaptive {
4263 light: "#F5F5FA".to_string(),
4264 dark: "#0D0D18".to_string(),
4265 },
4266 );
4267 tokens.bifrost.insert(
4269 "blur".to_string(),
4270 TokenValue::Single {
4271 value: "25.0".to_string(),
4272 },
4273 );
4274 tokens.bifrost.insert(
4275 "saturation".to_string(),
4276 TokenValue::Single {
4277 value: "1.2".to_string(),
4278 },
4279 );
4280 tokens.bifrost.insert(
4281 "opacity".to_string(),
4282 TokenValue::Single {
4283 value: "0.65".to_string(),
4284 },
4285 );
4286 tokens.gungnir.insert(
4288 "intensity".to_string(),
4289 TokenValue::Single {
4290 value: "1.0".to_string(),
4291 },
4292 );
4293 tokens.gungnir.insert(
4294 "radius".to_string(),
4295 TokenValue::Single {
4296 value: "15.0".to_string(),
4297 },
4298 );
4299 tokens.mjolnir.insert(
4301 "clip_angle".to_string(),
4302 TokenValue::Single {
4303 value: "12.0".to_string(),
4304 },
4305 );
4306 tokens.mjolnir.insert(
4307 "border_width".to_string(),
4308 TokenValue::Single {
4309 value: "2.0".to_string(),
4310 },
4311 );
4312 tokens.anim.insert(
4314 "stiffness".to_string(),
4315 TokenValue::Single {
4316 value: "170.0".to_string(),
4317 },
4318 );
4319 tokens.anim.insert(
4320 "damping".to_string(),
4321 TokenValue::Single {
4322 value: "26.0".to_string(),
4323 },
4324 );
4325 tokens.anim.insert(
4326 "mass".to_string(),
4327 TokenValue::Single {
4328 value: "1.0".to_string(),
4329 },
4330 );
4331 tokens.accessibility.insert(
4333 "reduce_motion".to_string(),
4334 TokenValue::Single {
4335 value: "false".to_string(),
4336 },
4337 );
4338 tokens
4339}
4340pub struct Environment<K: EnvKey> {
4342 _marker: std::marker::PhantomData<K>,
4343}
4344impl<K: EnvKey> Default for Environment<K> {
4345 fn default() -> Self {
4346 Self::new()
4347 }
4348}
4349impl<K: EnvKey> Environment<K> {
4350 pub fn new() -> Self {
4352 Self {
4353 _marker: std::marker::PhantomData,
4354 }
4355 }
4356 pub fn get(&self) -> K::Value {
4358 if let Some(env_store) = ENVIRONMENT.get() {
4359 let env_lock = env_store.lock().unwrap();
4360 if let Some(val) = env_lock.get(&std::any::TypeId::of::<K>()) {
4361 if let Some(typed_val) = val.downcast_ref::<K::Value>() {
4362 return typed_val.clone();
4363 } else {
4364 log::warn!(
4365 "Environment: Downcast failed for key type {:?}",
4366 std::any::type_name::<K>()
4367 );
4368 }
4369 } else {
4370 log::trace!(
4372 "Environment: Key not found: {:?}. Returning default.",
4373 std::any::type_name::<K>()
4374 );
4375 }
4376 } else {
4377 log::trace!(
4379 "Environment: Store not initialized. Key: {:?}. Returning default.",
4380 std::any::type_name::<K>()
4381 );
4382 }
4383 K::default_value()
4384 }
4385}
4386pub mod env {
4388 pub fn insert<K: super::EnvKey>(value: K::Value) {
4390 let store = super::ENVIRONMENT
4391 .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
4392 let mut env_map = store.lock().unwrap();
4393 env_map.insert(std::any::TypeId::of::<K>(), Box::new(value));
4394 }
4395 pub fn remove<K: super::EnvKey>() {
4397 if let Some(store) = super::ENVIRONMENT.get() {
4398 let mut env_map = store.lock().unwrap();
4399 env_map.remove(&std::any::TypeId::of::<K>());
4400 }
4401 }
4402}
4403#[derive(Debug, Clone, Copy, PartialEq)]
4406pub struct Size {
4407 pub width: f32,
4408 pub height: f32,
4409}
4410
4411impl Size {
4412 pub const ZERO: Self = Self {
4413 width: 0.0,
4414 height: 0.0,
4415 };
4416
4417 pub fn new(width: f32, height: f32) -> Self {
4418 Self { width, height }
4419 }
4420}
4421
4422#[derive(Debug, Clone, Copy, PartialEq)]
4424pub struct EdgeInsets {
4425 pub top: f32,
4426 pub leading: f32,
4427 pub bottom: f32,
4428 pub trailing: f32,
4429}
4430
4431impl EdgeInsets {
4432 pub fn all(value: f32) -> Self {
4434 Self {
4435 top: value,
4436 leading: value,
4437 bottom: value,
4438 trailing: value,
4439 }
4440 }
4441
4442 pub fn vertical(value: f32) -> Self {
4444 Self {
4445 top: value,
4446 leading: 0.0,
4447 bottom: value,
4448 trailing: 0.0,
4449 }
4450 }
4451
4452 pub fn horizontal(value: f32) -> Self {
4454 Self {
4455 top: 0.0,
4456 leading: value,
4457 bottom: 0.0,
4458 trailing: value,
4459 }
4460 }
4461}
4462
4463#[derive(Debug, Clone, Copy, PartialEq)]
4467pub struct FrameModifier {
4468 pub width: Option<f32>,
4470 pub height: Option<f32>,
4472 pub min_width: Option<f32>,
4474 pub max_width: Option<f32>,
4476 pub min_height: Option<f32>,
4478 pub max_height: Option<f32>,
4480 pub alignment: Alignment,
4482}
4483
4484impl Default for FrameModifier {
4485 fn default() -> Self {
4487 Self::new()
4488 }
4489}
4490
4491impl FrameModifier {
4492 pub fn new() -> Self {
4494 Self {
4495 width: None,
4496 height: None,
4497 min_width: None,
4498 max_width: None,
4499 min_height: None,
4500 max_height: None,
4501 alignment: Alignment::Center,
4502 }
4503 }
4504
4505 pub fn width(mut self, width: f32) -> Self {
4507 self.width = Some(width);
4508 self
4509 }
4510
4511 pub fn height(mut self, height: f32) -> Self {
4513 self.height = Some(height);
4514 self
4515 }
4516
4517 pub fn size(mut self, width: f32, height: f32) -> Self {
4519 self.width = Some(width);
4520 self.height = Some(height);
4521 self
4522 }
4523
4524 pub fn min_width(mut self, min_width: f32) -> Self {
4526 self.min_width = Some(min_width);
4527 self
4528 }
4529
4530 pub fn max_width(mut self, max_width: f32) -> Self {
4532 self.max_width = Some(max_width);
4533 self
4534 }
4535
4536 pub fn min_height(mut self, min_height: f32) -> Self {
4538 self.min_height = Some(min_height);
4539 self
4540 }
4541
4542 pub fn max_height(mut self, max_height: f32) -> Self {
4544 self.max_height = Some(max_height);
4545 self
4546 }
4547
4548 pub fn alignment(mut self, alignment: Alignment) -> Self {
4550 self.alignment = alignment;
4551 self
4552 }
4553}
4554
4555impl ViewModifier for FrameModifier {
4556 fn modify<V: View>(self, content: V) -> impl View {
4558 ModifiedView::new(content, self)
4559 }
4560
4561 fn transform_proposal(&self, proposal: SizeProposal) -> SizeProposal {
4563 let w = if let Some(width) = self.width {
4564 Some(width)
4565 } else {
4566 proposal.width.map(|pw| {
4567 pw.clamp(
4568 self.min_width.unwrap_or(0.0),
4569 self.max_width.unwrap_or(f32::INFINITY),
4570 )
4571 })
4572 };
4573 let h = if let Some(height) = self.height {
4574 Some(height)
4575 } else {
4576 proposal.height.map(|ph| {
4577 ph.clamp(
4578 self.min_height.unwrap_or(0.0),
4579 self.max_height.unwrap_or(f32::INFINITY),
4580 )
4581 })
4582 };
4583 SizeProposal {
4584 width: w,
4585 height: h,
4586 }
4587 }
4588
4589 fn transform_size(&self, child_size: Size) -> Size {
4591 let w = if let Some(width) = self.width {
4592 width
4593 } else {
4594 child_size.width.clamp(
4595 self.min_width.unwrap_or(0.0),
4596 self.max_width.unwrap_or(f32::INFINITY),
4597 )
4598 };
4599 let h = if let Some(height) = self.height {
4600 height
4601 } else {
4602 child_size.height.clamp(
4603 self.min_height.unwrap_or(0.0),
4604 self.max_height.unwrap_or(f32::INFINITY),
4605 )
4606 };
4607 Size {
4608 width: w,
4609 height: h,
4610 }
4611 }
4612
4613 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4615 self.render(renderer, rect);
4616 let child_proposal =
4617 self.transform_proposal(SizeProposal::new(Some(rect.width), Some(rect.height)));
4618 let child_size = view.intrinsic_size(renderer, child_proposal);
4619
4620 let mut child_x = rect.x;
4621 let mut child_y = rect.y;
4622
4623 match self.alignment {
4624 Alignment::Leading => {
4625 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4626 }
4627 Alignment::Trailing => {
4628 child_x = rect.x + rect.width - child_size.width;
4629 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4630 }
4631 Alignment::Top => {
4632 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4633 }
4634 Alignment::Bottom => {
4635 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4636 child_y = rect.y + rect.height - child_size.height;
4637 }
4638 Alignment::Center => {
4639 child_x = rect.x + (rect.width - child_size.width) / 2.0;
4640 child_y = rect.y + (rect.height - child_size.height) / 2.0;
4641 }
4642 }
4643
4644 let child_rect = Rect {
4645 x: child_x,
4646 y: child_y,
4647 width: child_size.width,
4648 height: child_size.height,
4649 };
4650
4651 view.render(renderer, child_rect);
4652 self.post_render(renderer, rect);
4653 }
4654}
4655
4656#[derive(Debug, Clone, Copy, PartialEq)]
4658pub struct FlexModifier {
4659 pub weight: f32,
4660}
4661
4662impl ViewModifier for FlexModifier {
4663 fn modify<V: View>(self, content: V) -> impl View {
4664 ModifiedView::new(content, self)
4665 }
4666
4667 fn child_flex_weight<V: View>(&self, _view: &V) -> f32 {
4668 self.weight
4669 }
4670}
4671
4672#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4674pub struct GridPlacementModifier {
4675 pub placement: GridPlacement,
4677}
4678
4679impl ViewModifier for GridPlacementModifier {
4680 fn modify<V: View>(self, content: V) -> impl View {
4682 ModifiedView::new(content, self)
4683 }
4684
4685 fn get_grid_placement(&self) -> Option<GridPlacement> {
4687 Some(self.placement)
4688 }
4689}
4690
4691#[derive(Clone)]
4694pub struct OverlayModifier {
4695 pub overlay: AnyView,
4697 pub alignment: Alignment,
4699 pub offset: [f32; 2],
4701 pub on_dismiss: Option<Arc<dyn Fn() + Send + Sync>>,
4703}
4704
4705impl ViewModifier for OverlayModifier {
4706 fn modify<V: View>(self, content: V) -> impl View {
4708 ModifiedView::new(content, self)
4709 }
4710
4711 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4713 view.render(renderer, rect);
4715
4716 let overlay_size = self
4718 .overlay
4719 .intrinsic_size(renderer, SizeProposal::unspecified());
4720
4721 let mut overlay_x;
4723 let mut overlay_y;
4724
4725 match self.alignment {
4726 Alignment::Leading => {
4727 overlay_x = rect.x - overlay_size.width;
4728 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4729 }
4730 Alignment::Trailing => {
4731 overlay_x = rect.x + rect.width;
4732 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4733 }
4734 Alignment::Top => {
4735 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4736 overlay_y = rect.y - overlay_size.height;
4737 }
4738 Alignment::Bottom => {
4739 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4740 overlay_y = rect.y + rect.height;
4741 }
4742 Alignment::Center => {
4743 overlay_x = rect.x + (rect.width - overlay_size.width) / 2.0;
4744 overlay_y = rect.y + (rect.height - overlay_size.height) / 2.0;
4745 }
4746 }
4747
4748 overlay_x += self.offset[0];
4749 overlay_y += self.offset[1];
4750
4751 let overlay_rect = Rect {
4752 x: overlay_x,
4753 y: overlay_y,
4754 width: overlay_size.width,
4755 height: overlay_size.height,
4756 };
4757
4758 if let Some(on_dismiss) = &self.on_dismiss {
4760 let dismiss = on_dismiss.clone();
4761 renderer.register_handler(
4762 "pointerdown",
4763 Arc::new(move |event| {
4764 if let Event::PointerDown { x, y, .. } = event {
4765 let click_inside = x >= overlay_rect.x
4766 && x <= overlay_rect.x + overlay_rect.width
4767 && y >= overlay_rect.y
4768 && y <= overlay_rect.y + overlay_rect.height;
4769 if !click_inside {
4770 dismiss();
4771 }
4772 }
4773 }),
4774 );
4775 }
4776
4777 self.overlay.render(renderer, overlay_rect);
4779 }
4780}
4781
4782#[derive(Debug, Clone, Copy, PartialEq)]
4784pub struct OffsetModifier {
4785 pub x: f32,
4786 pub y: f32,
4787}
4788
4789impl OffsetModifier {
4790 pub fn new(x: f32, y: f32) -> Self {
4791 Self { x, y }
4792 }
4793}
4794
4795impl ViewModifier for OffsetModifier {
4796 fn modify<V: View>(self, content: V) -> impl View {
4797 ModifiedView::new(content, self)
4798 }
4799}
4800
4801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4803pub struct ZIndexModifier {
4804 pub z_index: i32,
4805}
4806
4807impl ZIndexModifier {
4808 pub fn new(z_index: i32) -> Self {
4809 Self { z_index }
4810 }
4811}
4812
4813impl ViewModifier for ZIndexModifier {
4814 fn modify<V: View>(self, content: V) -> impl View {
4815 ModifiedView::new(content, self)
4816 }
4817}
4818
4819#[derive(Debug, Clone, Copy, PartialEq, Default)]
4821pub struct LayoutConstraints {
4822 pub min_width: Option<f32>,
4823 pub max_width: Option<f32>,
4824 pub min_height: Option<f32>,
4825 pub max_height: Option<f32>,
4826}
4827
4828#[derive(Debug, Clone, Copy, PartialEq)]
4830pub struct LayoutModifier {
4831 pub constraints: LayoutConstraints,
4832}
4833
4834impl LayoutModifier {
4835 pub fn new(constraints: LayoutConstraints) -> Self {
4836 Self { constraints }
4837 }
4838}
4839
4840impl ViewModifier for LayoutModifier {
4841 fn modify<V: View>(self, content: V) -> impl View {
4842 ModifiedView::new(content, self)
4843 }
4844}
4845
4846#[derive(Debug, Clone, Copy, PartialEq)]
4848pub struct SafeAreaModifier {
4849 pub ignores: bool,
4850}
4851
4852impl ViewModifier for SafeAreaModifier {
4853 fn modify<V: View>(self, content: V) -> impl View {
4854 ModifiedView::new(content, self)
4855 }
4856}
4857
4858#[derive(Debug, Clone, Copy, PartialEq)]
4860pub struct ElevationModifier {
4861 pub level: f32,
4862}
4863
4864impl ViewModifier for ElevationModifier {
4865 fn modify<V: View>(self, content: V) -> impl View {
4866 ModifiedView::new(content, self)
4867 }
4868
4869 fn render_view<V: View>(&self, view: &V, renderer: &mut dyn Renderer, rect: Rect) {
4870 if self.level > 0.0 {
4871 let radius = self.level * 2.0;
4872 let offset_y = self.level * 0.5;
4873 let shadow_color = [0.0, 0.0, 0.0, 0.3];
4874 renderer.push_shadow(radius, shadow_color, [0.0, offset_y]);
4875 view.render(renderer, rect);
4876 renderer.pop_shadow();
4877 } else {
4878 view.render(renderer, rect);
4879 }
4880 }
4881}
4882
4883pub mod layout {
4885 use super::*;
4886
4887 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4890 pub struct LayoutKey {
4891 pub view_hash: u64,
4892 pub generation: u64,
4893 }
4894
4895 pub struct LayoutCache {
4897 pub safe_area: SafeArea,
4898 pub delta_time: f32,
4899 pub scale_factor: f32,
4901 pub viewport: Option<Rect>,
4904 pub layout_time_budget: std::time::Duration,
4906 pub layout_start_time: Option<std::time::Instant>,
4908 size_cache: HashMap<(u64, u32, u32), Size>, pub parent_map: HashMap<u64, u64>,
4911 generation: u64,
4915 pub engine: Option<Box<dyn std::any::Any + Send + Sync>>,
4917 pub animators: Option<Box<dyn std::any::Any + Send + Sync>>,
4919 pub previous_rects: HashMap<u64, Rect>,
4921 pub eviction_generation: u64,
4924 pub previous_rects_generation: HashMap<u64, u64>,
4926 eviction_threshold: u64,
4928 }
4929
4930 thread_local! {
4931 static LAYOUT_BUDGET_DEADLINE: std::cell::RefCell<Option<std::time::Instant>> =
4932 const { std::cell::RefCell::new(None) };
4933 }
4934
4935 impl Default for LayoutCache {
4936 fn default() -> Self {
4937 Self::new()
4938 }
4939 }
4940
4941 impl LayoutCache {
4942 pub fn new() -> Self {
4943 Self {
4944 safe_area: SafeArea::default(),
4945 delta_time: 0.016,
4946 scale_factor: 1.0,
4947 viewport: None,
4948 layout_time_budget: std::time::Duration::from_millis(4),
4949 layout_start_time: None,
4950 size_cache: HashMap::new(),
4951 parent_map: HashMap::new(),
4952 generation: 0,
4953 engine: None,
4954 animators: None,
4955 previous_rects: HashMap::new(),
4956 eviction_generation: 0,
4957 previous_rects_generation: HashMap::new(),
4958 eviction_threshold: 300, }
4960 }
4961
4962 pub fn generation(&self) -> u64 {
4964 self.generation
4965 }
4966
4967 pub fn evict_stale_entries(&mut self) {
4969 self.eviction_generation += 1;
4970 let threshold = self.eviction_threshold;
4971 let current_gen = self.eviction_generation;
4972 self.previous_rects.retain(|hash, _| {
4973 self.previous_rects_generation
4974 .get(hash)
4975 .map_or(false, |g| current_gen - *g < threshold)
4976 });
4977 self.previous_rects_generation
4978 .retain(|hash, _| self.previous_rects.contains_key(hash));
4979 }
4980
4981 pub fn is_over_budget(&self) -> bool {
4983 let deadline_red = LAYOUT_BUDGET_DEADLINE.with(|deadline| {
4984 deadline.borrow().as_ref().is_some_and(|deadline| std::time::Instant::now() >= *deadline)
4985 });
4986 if deadline_red {
4987 return true;
4988 }
4989 if let Some(start) = self.layout_start_time {
4990 start.elapsed() > self.layout_time_budget
4991 } else {
4992 false
4993 }
4994 }
4995
4996 pub fn set_layout_budget_deadline(deadline: Option<std::time::Instant>) {
5000 LAYOUT_BUDGET_DEADLINE.with(|slot| {
5001 *slot.borrow_mut() = deadline;
5002 });
5003 }
5004
5005 pub fn clear_layout_budget_deadline() {
5007 Self::set_layout_budget_deadline(None);
5008 }
5009
5010 pub fn invalidate(&mut self) {
5014 self.generation = self.generation.wrapping_add(1);
5015 }
5016
5017 pub fn is_valid(&self, key: LayoutKey, current_gen: u64) -> bool {
5020 key.generation == current_gen && key.generation == self.generation
5021 }
5022
5023 pub fn clear(&mut self) {
5024 self.safe_area = SafeArea::default();
5025 self.viewport = None;
5026 self.layout_start_time = None;
5027 self.size_cache.clear();
5028 self.parent_map.clear();
5029 }
5030
5031 pub fn get_size(&self, view_hash: u64, proposal: SizeProposal) -> Option<Size> {
5032 let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5033 let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5034 self.size_cache.get(&(view_hash, pw, ph)).copied()
5035 }
5036
5037 pub fn set_size(&mut self, view_hash: u64, proposal: SizeProposal, size: Size) {
5038 let pw = (proposal.width.unwrap_or(-1.0) * 100.0) as u32;
5039 let ph = (proposal.height.unwrap_or(-1.0) * 100.0) as u32;
5040 self.size_cache.insert((view_hash, pw, ph), size);
5041 }
5042
5043 pub fn register_parent(&mut self, child_hash: u64, parent_hash: u64) {
5045 if child_hash != 0 && parent_hash != 0 {
5046 self.parent_map.insert(child_hash, parent_hash);
5047 }
5048 }
5049
5050 pub fn invalidate_view(&mut self, view_hash: u64) {
5053 let mut to_invalidate = vec![view_hash];
5054 let mut visited = std::collections::HashSet::new();
5055 while let Some(hash) = to_invalidate.pop() {
5056 if !visited.insert(hash) {
5057 continue;
5058 }
5059 self.size_cache.retain(|&(h, _, _), _| h != hash);
5060 if let Some(&parent) = self.parent_map.get(&hash) {
5061 to_invalidate.push(parent);
5062 }
5063 }
5064 }
5065 }
5066
5067 #[derive(Debug, Clone, Copy, PartialEq)]
5069 pub struct SizeProposal {
5070 pub width: Option<f32>,
5071 pub height: Option<f32>,
5072 }
5073
5074 impl SizeProposal {
5075 pub fn unspecified() -> Self {
5076 Self {
5077 width: None,
5078 height: None,
5079 }
5080 }
5081
5082 pub fn width(width: f32) -> Self {
5083 Self {
5084 width: Some(width),
5085 height: None,
5086 }
5087 }
5088
5089 pub fn height(height: f32) -> Self {
5090 Self {
5091 width: None,
5092 height: Some(height),
5093 }
5094 }
5095
5096 pub fn tight(width: f32, height: f32) -> Self {
5097 Self {
5098 width: Some(width),
5099 height: Some(height),
5100 }
5101 }
5102
5103 pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
5104 Self { width, height }
5105 }
5106 }
5107
5108 pub trait LayoutView: Send {
5110 fn size_that_fits(
5112 &self,
5113 proposal: SizeProposal,
5114 subviews: &[&dyn LayoutView],
5115 cache: &mut LayoutCache,
5116 ) -> Size;
5117
5118 fn place_subviews(
5120 &self,
5121 bounds: Rect,
5122 subviews: &mut [&mut dyn LayoutView],
5123 cache: &mut LayoutCache,
5124 );
5125
5126 fn flex_weight(&self) -> f32 {
5128 0.0
5129 }
5130
5131 fn view_hash(&self) -> u64 {
5134 0
5135 }
5136
5137 fn changed(&self) -> bool {
5146 true
5147 }
5148
5149 fn debug_layout(&self, indent: usize) -> String {
5152 let prefix = " ".repeat(indent);
5153 format!("{}LayoutView", prefix)
5154 }
5155 }
5156 #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5158 pub struct EdgeInsets {
5159 pub top: f32,
5160 pub leading: f32,
5161 pub bottom: f32,
5162 pub trailing: f32,
5163 }
5164
5165 impl EdgeInsets {
5166 pub fn new(top: f32, leading: f32, bottom: f32, trailing: f32) -> Self {
5167 Self {
5168 top,
5169 leading,
5170 bottom,
5171 trailing,
5172 }
5173 }
5174
5175 pub fn all(value: f32) -> Self {
5176 Self {
5177 top: value,
5178 leading: value,
5179 bottom: value,
5180 trailing: value,
5181 }
5182 }
5183 }
5184
5185 #[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
5187 pub struct SafeArea {
5188 pub insets: EdgeInsets,
5189 }
5190
5191 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5193 pub enum SdfShape {
5194 Rect(Rect),
5195 RoundedRect { rect: Rect, radius: f32 },
5196 Circle { center: [f32; 2], radius: f32 },
5197 }
5198
5199 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
5201 pub struct Rect {
5202 pub x: f32,
5203 pub y: f32,
5204 pub width: f32,
5205 pub height: f32,
5206 }
5207
5208 impl Rect {
5209 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
5210 Self {
5211 x,
5212 y,
5213 width,
5214 height,
5215 }
5216 }
5217
5218 pub fn inset(&self, amount: f32) -> Self {
5219 Self {
5220 x: self.x + amount,
5221 y: self.y + amount,
5222 width: (self.width - amount * 2.0).max(0.0),
5223 height: (self.height - amount * 2.0).max(0.0),
5224 }
5225 }
5226
5227 pub fn offset(&self, dx: f32, dy: f32) -> Self {
5228 Self {
5229 x: self.x + dx,
5230 y: self.y + dy,
5231 ..*self
5232 }
5233 }
5234
5235 pub fn zero() -> Self {
5236 Self {
5237 x: 0.0,
5238 y: 0.0,
5239 width: 0.0,
5240 height: 0.0,
5241 }
5242 }
5243
5244 pub fn contains(&self, x: f32, y: f32) -> bool {
5245 x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
5246 }
5247
5248 pub fn intersects(&self, other: &Rect) -> bool {
5255 self.x < other.x + other.width
5256 && self.x + self.width > other.x
5257 && self.y < other.y + other.height
5258 && self.y + self.height > other.y
5259 }
5260
5261 pub fn size(&self) -> Size {
5262 Size {
5263 width: self.width,
5264 height: self.height,
5265 }
5266 }
5267
5268 pub fn split_horizontal(&self, n: usize) -> Vec<Rect> {
5270 if n == 0 {
5271 return vec![];
5272 }
5273 let item_width = self.width / n as f32;
5274 (0..n)
5275 .map(|i| Rect {
5276 x: self.x + i as f32 * item_width,
5277 y: self.y,
5278 width: item_width,
5279 height: self.height,
5280 })
5281 .collect()
5282 }
5283
5284 pub fn split_vertical(&self, n: usize) -> Vec<Rect> {
5286 if n == 0 {
5287 return vec![];
5288 }
5289 let item_height = self.height / n as f32;
5290 (0..n)
5291 .map(|i| Rect {
5292 x: self.x,
5293 y: self.y + i as f32 * item_height,
5294 width: self.width,
5295 height: item_height,
5296 })
5297 .collect()
5298 }
5299 }
5300}
5301
5302pub mod agents;
5305pub mod animation;
5306pub mod gpu;
5307pub mod material;
5308pub mod runtime;
5309pub mod scene_graph;
5310pub mod sdf_shadow;
5311
5312pub use layout::{LayoutCache, LayoutKey, LayoutView, Rect, SizeProposal};
5314pub use material::DrawMaterial;
5315pub use scene_graph::{NodeId, bifrost_registry};
5316pub use color::SemanticColors;
5317
5318pub trait AssetManager: Send + Sync {
5322 fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>>;
5324
5325 fn preload_image(&self, url: &str);
5327}
5328
5329#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5331pub enum TouchPhase {
5332 Began,
5334 Moved,
5336 Ended,
5338 Cancelled,
5340}
5341
5342#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
5344pub enum Event {
5345 PointerDown {
5346 x: f32,
5347 y: f32,
5348 button: u32,
5349 proximity_field: f32,
5350 tilt: Option<f32>,
5351 azimuth: Option<f32>,
5352 pressure: Option<f32>,
5353 barrel_rotation: Option<f32>,
5354 pointer_precision: f32,
5355 },
5356 PointerUp {
5357 x: f32,
5358 y: f32,
5359 button: u32,
5360 tilt: Option<f32>,
5361 azimuth: Option<f32>,
5362 pressure: Option<f32>,
5363 barrel_rotation: Option<f32>,
5364 pointer_precision: f32,
5365 },
5366 PointerMove {
5367 x: f32,
5368 y: f32,
5369 proximity_field: f32,
5370 tilt: Option<f32>,
5371 azimuth: Option<f32>,
5372 pressure: Option<f32>,
5373 barrel_rotation: Option<f32>,
5374 pointer_precision: f32,
5375 },
5376 PointerClick {
5377 x: f32,
5378 y: f32,
5379 button: u32,
5380 tilt: Option<f32>,
5381 azimuth: Option<f32>,
5382 pressure: Option<f32>,
5383 barrel_rotation: Option<f32>,
5384 pointer_precision: f32,
5385 },
5386 PointerEnter,
5387 PointerLeave,
5388 PointerWheel {
5391 x: f32,
5392 y: f32,
5393 delta_x: f32,
5394 delta_y: f32,
5395 pointer_precision: f32,
5396 },
5397 PointerDoubleClick {
5399 x: f32,
5400 y: f32,
5401 button: u32,
5402 pointer_precision: f32,
5403 },
5404 DragStart {
5406 x: f32,
5407 y: f32,
5408 button: u32,
5409 pointer_precision: f32,
5410 },
5411 DragMove {
5413 x: f32,
5414 y: f32,
5415 pointer_precision: f32,
5416 },
5417 DragEnd {
5419 x: f32,
5420 y: f32,
5421 pointer_precision: f32,
5422 },
5423 KeyDown {
5424 key: String,
5425 modifiers: KeyModifiers,
5426 },
5427 KeyUp {
5428 key: String,
5429 modifiers: KeyModifiers,
5430 },
5431 FocusIn,
5433 FocusOut,
5435 Copy,
5437 Cut,
5439 Paste(String),
5441 Ime(String),
5443 TouchStart {
5445 x: f32,
5446 y: f32,
5447 touch_id: u64,
5448 },
5449 TouchMove {
5451 x: f32,
5452 y: f32,
5453 touch_id: u64,
5454 },
5455 TouchEnd {
5457 x: f32,
5458 y: f32,
5459 touch_id: u64,
5460 },
5461 TouchCancel {
5463 touch_id: u64,
5464 },
5465 GesturePinch {
5471 center: [f32; 2],
5472 scale: f32,
5473 velocity: f32,
5474 phase: TouchPhase,
5475 },
5476 GestureSwipe {
5481 direction: [f32; 2],
5482 velocity: f32,
5483 phase: TouchPhase,
5484 },
5485 FileDrop {
5487 x: f32,
5488 y: f32,
5489 path: String,
5490 },
5491}
5492
5493impl Event {
5494 pub fn pointer_precision(&self) -> f32 {
5500 match self {
5501 Self::PointerDown {
5502 pointer_precision, ..
5503 }
5504 | Self::PointerUp {
5505 pointer_precision, ..
5506 }
5507 | Self::PointerMove {
5508 pointer_precision, ..
5509 }
5510 | Self::PointerClick {
5511 pointer_precision, ..
5512 }
5513 | Self::PointerWheel {
5514 pointer_precision, ..
5515 }
5516 | Self::PointerDoubleClick {
5517 pointer_precision, ..
5518 }
5519 | Self::DragStart {
5520 pointer_precision, ..
5521 }
5522 | Self::DragMove {
5523 pointer_precision, ..
5524 }
5525 | Self::DragEnd {
5526 pointer_precision, ..
5527 } => *pointer_precision,
5528 _ => 0.0,
5529 }
5530 }
5531
5532 pub fn name(&self) -> &'static str {
5534 match self {
5535 Self::PointerDown { .. } => "pointerdown",
5536 Self::PointerUp { .. } => "pointerup",
5537 Self::PointerMove { .. } => "pointermove",
5538 Self::PointerClick { .. } => "pointerclick",
5539 Self::PointerEnter => "pointerenter",
5540 Self::PointerLeave => "pointerleave",
5541 Self::PointerWheel { .. } => "pointerwheel",
5542 Self::PointerDoubleClick { .. } => "pointerdoubleclick",
5543 Self::DragStart { .. } => "dragstart",
5544 Self::DragMove { .. } => "dragmove",
5545 Self::DragEnd { .. } => "dragend",
5546 Self::KeyDown { .. } => "keydown",
5547 Self::KeyUp { .. } => "keyup",
5548 Self::FocusIn => "focusin",
5549 Self::FocusOut => "focusout",
5550 Self::Copy => "copy",
5551 Self::Cut => "cut",
5552 Self::Paste(_) => "paste",
5553 Self::Ime(_) => "ime",
5554 Self::TouchStart { .. } => "touchstart",
5555 Self::TouchMove { .. } => "touchmove",
5556 Self::TouchEnd { .. } => "touchend",
5557 Self::TouchCancel { .. } => "touchcancel",
5558 Self::GesturePinch { .. } => "gesturepinch",
5559 Self::GestureSwipe { .. } => "gestureswipe",
5560 Self::FileDrop { .. } => "filedrop",
5561 }
5562 }
5563}
5564
5565
5566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5568pub enum EventResponse {
5569 Handled,
5570 Ignored,
5571}
5572
5573#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5604pub enum EventPhase {
5605 Capture,
5607 Target,
5609 Bubble,
5611}
5612
5613impl EventPhase {
5614 pub const ALL: [EventPhase; 3] = [
5616 EventPhase::Capture,
5617 EventPhase::Target,
5618 EventPhase::Bubble,
5619 ];
5620}
5621
5622pub struct DefaultAssetManager {
5624 cache: AssetCache,
5625}
5626type AssetCache = Arc<arc_swap::ArcSwap<HashMap<String, AssetState<Arc<Vec<u8>>>>>>;
5627
5628impl Default for DefaultAssetManager {
5629 fn default() -> Self {
5630 Self::new()
5631 }
5632}
5633
5634impl DefaultAssetManager {
5635 pub fn new() -> Self {
5636 Self {
5637 cache: Arc::new(arc_swap::ArcSwap::from_pointee(HashMap::new())),
5638 }
5639 }
5640}
5641
5642impl AssetManager for DefaultAssetManager {
5643 fn load_image(&self, url: &str) -> AssetState<Arc<Vec<u8>>> {
5644 if let Some(state) = self.cache.load().get(url) {
5645 return state.clone();
5646 }
5647
5648 self.cache.rcu(|map| {
5649 let mut m = (**map).clone();
5650 m.entry(url.to_string()).or_insert(AssetState::Loading);
5651 m
5652 });
5653 AssetState::Loading
5654 }
5655
5656 fn preload_image(&self, _url: &str) {}
5657}
5658
5659use std::future::Future;
5660
5661pub struct Suspense<T: Clone + Send + Sync + 'static> {
5664 inner: State<AssetState<T>>,
5665}
5666
5667impl<T: Clone + Send + Sync + 'static> Default for Suspense<T> {
5668 fn default() -> Self {
5669 Self::new()
5670 }
5671}
5672
5673impl<T: Clone + Send + Sync + 'static> Suspense<T> {
5674 pub fn new() -> Self {
5675 Self {
5676 inner: State::new(AssetState::Loading),
5677 }
5678 }
5679
5680 pub fn new_async<F>(future: F) -> Self
5681 where
5682 F: Future<Output = Result<T, String>> + Send + 'static,
5683 {
5684 let suspense = Self::new();
5685 let suspense_clone = suspense.clone();
5686
5687 #[cfg(not(target_arch = "wasm32"))]
5688 {
5689 if let Ok(handle) = tokio::runtime::Handle::try_current() {
5695 handle.spawn(async move {
5696 let result = future.await;
5697 match result {
5698 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5699 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5700 }
5701 });
5702 } else {
5703 fallback_runtime().spawn(async move {
5704 let result = future.await;
5705 match result {
5706 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5707 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5708 }
5709 });
5710 }
5711 }
5712 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
5713 {
5714 wasm_bindgen_futures::spawn_local(async move {
5715 let result = future.await;
5716 match result {
5717 Ok(val) => suspense_clone.inner.set(AssetState::Ready(val)),
5718 Err(err) => suspense_clone.inner.set(AssetState::Error(err)),
5719 }
5720 });
5721 }
5722
5723 suspense
5724 }
5725
5726 pub fn ready(value: T) -> Self {
5727 Self {
5728 inner: State::new(AssetState::Ready(value)),
5729 }
5730 }
5731
5732 pub fn error(message: impl Into<String>) -> Self {
5733 Self {
5734 inner: State::new(AssetState::Error(message.into())),
5735 }
5736 }
5737
5738 pub fn get(&self) -> AssetState<T> {
5739 self.inner.get()
5740 }
5741
5742 pub fn get_ref(&self) -> AssetState<T> {
5743 self.inner.get()
5744 }
5745
5746 pub fn is_loading(&self) -> bool {
5747 matches!(self.get(), AssetState::Loading)
5748 }
5749
5750 pub fn is_ready(&self) -> bool {
5751 matches!(self.get(), AssetState::Ready(_))
5752 }
5753
5754 pub fn is_error(&self) -> bool {
5755 matches!(self.get(), AssetState::Error(_))
5756 }
5757
5758 pub fn ready_value(&self) -> Option<T> {
5759 match self.get() {
5760 AssetState::Ready(value) => Some(value),
5761 _ => None,
5762 }
5763 }
5764
5765 pub fn error_message(&self) -> Option<String> {
5766 match self.get() {
5767 AssetState::Error(message) => Some(message),
5768 _ => None,
5769 }
5770 }
5771
5772 pub fn subscribe<F: Fn(&AssetState<T>) + Send + Sync + 'static>(&self, callback: F) {
5773 self.inner.subscribe(callback)
5774 }
5775
5776 pub fn inner_state(&self) -> &State<AssetState<T>> {
5777 &self.inner
5778 }
5779}
5780
5781impl<T: Clone + Send + Sync + 'static> Clone for Suspense<T> {
5782 fn clone(&self) -> Self {
5783 Self {
5784 inner: self.inner.clone(),
5785 }
5786 }
5787}
5788
5789impl<T: Clone + Send + Sync + 'static> From<T> for Suspense<T> {
5790 fn from(value: T) -> Self {
5791 Self::ready(value)
5792 }
5793}
5794
5795impl<T: Clone + Send + Sync + 'static> From<Result<T, String>> for Suspense<T> {
5796 fn from(result: Result<T, String>) -> Self {
5797 match result {
5798 Ok(value) => Self::ready(value),
5799 Err(error) => Self::error(error),
5800 }
5801 }
5802}
5803
5804#[cfg(test)]
5805mod phase1_test;
5806
5807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5809pub enum BerserkerMode {
5810 Normal,
5811 Rage, Frenzy, GodMode, }
5815
5816pub trait Seer: Send + Sync {
5819 fn predict(&self, context: &str) -> String;
5821 fn whispers(&self) -> Vec<String>;
5823}
5824
5825#[cfg(test)]
5826mod vili_tests {
5827 use super::*;
5828
5829 struct DummyRenderer;
5830 impl ElapsedTime for DummyRenderer {
5831 fn elapsed_time(&self) -> f32 {
5832 0.0
5833 }
5834 fn delta_time(&self) -> f32 {
5835 0.0
5836 }
5837 }
5838 impl Renderer for DummyRenderer {
5839 fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
5840 fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
5841 fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
5842 fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5843 fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
5844 fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
5845 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
5846
5847 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
5848 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
5849 }
5850 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
5851 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
5852 fn pop_transform_3d(&mut self) {}
5853 }
5854
5855 #[test]
5856 fn test_magnetic_warp() {
5857 let renderer = DummyRenderer;
5858 let anchor = Rect {
5859 x: 100.0,
5860 y: 100.0,
5861 width: 50.0,
5862 height: 50.0,
5863 };
5864 let pointer = [125.0, 50.0];
5866 let warp = renderer.magnetic_warp(pointer, anchor, 1.0);
5869 assert!(warp[1] > 50.0);
5871
5872 let far_pointer = [500.0, 500.0];
5874 let far_warp = renderer.magnetic_warp(far_pointer, anchor, 1.0);
5875 assert_eq!(far_pointer, far_warp);
5876 }
5877
5878 #[test]
5879 fn test_mani_glow() {
5880 let renderer = DummyRenderer;
5881 let bounds = Rect {
5882 x: 0.0,
5883 y: 0.0,
5884 width: 100.0,
5885 height: 100.0,
5886 };
5887 let pointer_inside = [50.0, 50.0];
5888 let glow_max = renderer.mani_glow_intensity(pointer_inside, bounds, 120.0);
5889 assert_eq!(glow_max, 1.0);
5890
5891 let pointer_edge = [50.0, -10.0];
5892 let glow_partial = renderer.mani_glow_intensity(pointer_edge, bounds, 120.0);
5893 assert!(glow_partial > 0.0 && glow_partial < 1.0);
5894 }
5895
5896 #[test]
5897 fn test_fafnir_evolve() {
5898 let renderer = DummyRenderer;
5899 let bounds = Rect {
5900 x: 0.0,
5901 y: 0.0,
5902 width: 100.0,
5903 height: 100.0,
5904 };
5905 let pointer_inside = [50.0, 50.0];
5906 let scale = renderer.fafnir_evolve(pointer_inside, bounds, 1.2);
5907 assert_eq!(scale, 1.2); }
5909
5910 #[test]
5911 fn test_undo_manager_basic() {
5912 let mut manager = UndoManager::new(3, 0.5);
5913 let val = std::sync::Arc::new(std::sync::Mutex::new(0));
5914
5915 let v1 = val.clone();
5916 let v2 = val.clone();
5917 manager.push(
5918 "Add",
5919 move || *v1.lock().unwrap() -= 1,
5920 move || *v2.lock().unwrap() += 1,
5921 );
5922
5923 assert!(manager.can_undo());
5924 assert!(!manager.can_redo());
5925
5926 let undo = manager.undo().unwrap();
5927 undo();
5928 assert_eq!(*val.lock().unwrap(), -1);
5929 assert!(!manager.can_undo());
5930 assert!(manager.can_redo());
5931
5932 let redo = manager.redo().unwrap();
5933 redo();
5934 assert_eq!(*val.lock().unwrap(), 0);
5935 }
5936
5937 #[test]
5938 fn test_undo_manager_depth_limit() {
5939 let mut manager = UndoManager::new(2, 0.5);
5940 manager.push("1", || {}, || {});
5941 manager.push("2", || {}, || {});
5942 manager.push("3", || {}, || {});
5943
5944 assert_eq!(manager.stack.len(), 2);
5945 assert_eq!(manager.position, 2);
5946 }
5947
5948 #[test]
5949 fn test_undo_manager_coalescing() {
5950 let mut manager = UndoManager::new(10, 1.0);
5951 let count = std::sync::Arc::new(std::sync::Mutex::new(0));
5952
5953 let c = count.clone();
5954 manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
5955
5956 let c = count.clone();
5957 manager.push_coalesceable("Type", move || *c.lock().unwrap() -= 1, || {});
5958
5959 assert_eq!(manager.stack.len(), 1);
5960
5961 let undo = manager.undo().unwrap();
5962 undo();
5963 assert_eq!(*count.lock().unwrap(), -2);
5964 }
5965}
5966
5967#[cfg(test)]
5968mod error_boundary_tests {
5969 use super::*;
5970
5971 struct SuccessView;
5973
5974 impl View for SuccessView {
5975 type Body = Never;
5976 fn body(self) -> Never {
5977 unreachable!()
5979 }
5980 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
5981 }
5983 }
5984
5985 struct PanicOnRender;
5987
5988 impl View for PanicOnRender {
5989 type Body = Never;
5990 fn body(self) -> Never {
5991 unreachable!()
5993 }
5994 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
5995 panic!("intentional render panic");
5996 }
5997 }
5998
5999 struct PanicOnSize;
6001
6002 impl View for PanicOnSize {
6003 type Body = Never;
6004 fn body(self) -> Never {
6005 unreachable!()
6007 }
6008 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6009 }
6011 fn intrinsic_size(&self, _renderer: &mut dyn Renderer, _proposal: SizeProposal) -> Size {
6012 panic!("intentional size panic");
6013 }
6014 }
6015
6016 struct PanicWithString;
6018
6019 impl View for PanicWithString {
6020 type Body = Never;
6021 fn body(self) -> Never {
6022 unreachable!()
6024 }
6025 fn render(&self, _renderer: &mut dyn Renderer, _rect: Rect) {
6026 panic!("{}", "custom error message".to_string());
6027 }
6028 }
6029
6030 struct DummyRenderer;
6031 impl ElapsedTime for DummyRenderer {
6032 fn elapsed_time(&self) -> f32 {
6033 0.0
6034 }
6035 fn delta_time(&self) -> f32 {
6036 0.0
6037 }
6038 }
6039 impl Renderer for DummyRenderer {
6040 fn fill_rect(&mut self, _r: Rect, _c: [f32; 4]) {}
6041 fn fill_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4]) {}
6042 fn fill_ellipse(&mut self, _r: Rect, _c: [f32; 4]) {}
6043 fn stroke_rect(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6044 fn stroke_rounded_rect(&mut self, _r: Rect, _rad: f32, _c: [f32; 4], _w: f32) {}
6045 fn stroke_ellipse(&mut self, _r: Rect, _c: [f32; 4], _w: f32) {}
6046 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6047
6048 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6049 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
6050 }
6051 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6052 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6053 fn pop_transform_3d(&mut self) {}
6054 }
6055
6056 const TEST_RECT: Rect = Rect {
6057 x: 0.0,
6058 y: 0.0,
6059 width: 200.0,
6060 height: 100.0,
6061 };
6062
6063 #[test]
6064 fn error_boundary_renders_child_on_success() {
6065 let boundary = ErrorBoundary::new(SuccessView);
6066 let mut renderer = DummyRenderer;
6067
6068 boundary.render(&mut renderer, TEST_RECT);
6069
6070 assert!(
6071 !boundary.has_error(),
6072 "should not have error after successful render"
6073 );
6074 assert!(
6075 boundary.last_error().is_none(),
6076 "should have no error message"
6077 );
6078 }
6079
6080 #[test]
6081 fn error_boundary_catches_render_panic() {
6082 let boundary = ErrorBoundary::new(PanicOnRender);
6083 let mut renderer = DummyRenderer;
6084
6085 boundary.render(&mut renderer, TEST_RECT);
6087
6088 assert!(
6089 boundary.has_error(),
6090 "should have error after catching panic"
6091 );
6092 let err = boundary.last_error().expect("should have error message");
6093 assert!(
6094 err.contains("intentional render panic"),
6095 "error message should contain panic message, got: {err}"
6096 );
6097 }
6098
6099 #[test]
6100 fn error_boundary_catches_size_panic() {
6101 let boundary = ErrorBoundary::new(PanicOnSize);
6102 let mut renderer = DummyRenderer;
6103 let proposal = layout::SizeProposal {
6104 width: Some(100.0),
6105 height: Some(50.0),
6106 };
6107
6108 let size = boundary.intrinsic_size(&mut renderer, proposal);
6109
6110 assert!(
6111 boundary.has_error(),
6112 "should have error after catching size panic"
6113 );
6114 assert_eq!(size, Size::ZERO, "fallback size should be zero");
6115 }
6116
6117 #[test]
6118 fn error_boundary_catches_string_panic() {
6119 let boundary = ErrorBoundary::new(PanicWithString);
6120 let mut renderer = DummyRenderer;
6121
6122 boundary.render(&mut renderer, TEST_RECT);
6123
6124 assert!(boundary.has_error());
6125 let err = boundary.last_error().expect("should have error message");
6126 assert!(
6127 err.contains("custom error message"),
6128 "should capture String panic payload, got: {err}"
6129 );
6130 }
6131
6132 #[test]
6133 fn error_boundary_clear_error_resets_state() {
6134 let boundary = ErrorBoundary::new(PanicOnRender);
6135 let mut renderer = DummyRenderer;
6136
6137 boundary.render(&mut renderer, TEST_RECT);
6138 assert!(boundary.has_error());
6139
6140 boundary.clear_error();
6141 assert!(
6142 !boundary.has_error(),
6143 "should be clear after clear_error()"
6144 );
6145 assert!(
6146 boundary.last_error().is_none(),
6147 "error message should be cleared"
6148 );
6149 }
6150
6151 #[test]
6152 fn error_boundary_fallback_color_is_configurable() {
6153 let boundary = ErrorBoundary::new(SuccessView)
6154 .fallback_color([0.0, 0.0, 1.0, 1.0])
6155 .fallback_label("custom label");
6156
6157 assert_eq!(boundary.fallback_color, [0.0, 0.0, 1.0, 1.0]);
6158 assert_eq!(
6159 boundary.fallback_label.as_deref(),
6160 Some("custom label")
6161 );
6162 }
6163
6164 #[test]
6165 fn error_boundary_flex_weight_delegates_to_child() {
6166 let boundary = ErrorBoundary::new(SuccessView);
6167 assert_eq!(boundary.flex_weight(), 0.0, "should delegate to child (default 0.0)");
6168 }
6169
6170 #[test]
6171 fn error_boundary_body_delegates_to_child() {
6172 let _boundary = ErrorBoundary::new(SuccessView);
6174 let _boundary_type = std::any::type_name::<ErrorBoundary<SuccessView>>();
6178 }
6179
6180 struct TrackingRenderer {
6183 clip_depth: u32,
6184 opacity_depth: u32,
6185 shadow_depth: u32,
6186 }
6187
6188 impl TrackingRenderer {
6189 fn new() -> Self {
6190 Self {
6191 clip_depth: 0,
6192 opacity_depth: 0,
6193 shadow_depth: 0,
6194 }
6195 }
6196 }
6197
6198 impl Renderer for TrackingRenderer {
6199 fn fill_rect(&mut self, _rect: Rect, _color: [f32; 4]) {}
6200 fn fill_rounded_rect(&mut self, _rect: Rect, _radius: f32, _color: [f32; 4]) {}
6201 fn fill_ellipse(&mut self, _rect: Rect, _color: [f32; 4]) {}
6202 fn stroke_rect(&mut self, _rect: Rect, _color: [f32; 4], _w: f32) {}
6203 fn stroke_rounded_rect(
6204 &mut self,
6205 _rect: Rect,
6206 _radius: f32,
6207 _color: [f32; 4],
6208 _stroke_width: f32,
6209 ) {
6210 }
6211 fn stroke_ellipse(&mut self, _rect: Rect, _color: [f32; 4], _stroke_width: f32) {}
6212 fn draw_line(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _c: [f32; 4], _w: f32) {}
6213
6214 fn push_clip_rect(&mut self, _rect: Rect) {
6215 self.clip_depth += 1;
6216 }
6217 fn pop_clip_rect(&mut self) {
6218 self.clip_depth = self.clip_depth.saturating_sub(1);
6219 }
6220 fn push_opacity(&mut self, _opacity: f32) {
6221 self.opacity_depth += 1;
6222 }
6223 fn pop_opacity(&mut self) {
6224 self.opacity_depth = self.opacity_depth.saturating_sub(1);
6225 }
6226 fn push_shadow(&mut self, _r: f32, _c: [f32; 4], _o: [f32; 2]) {
6227 self.shadow_depth += 1;
6228 }
6229 fn pop_shadow(&mut self) {
6230 self.shadow_depth = self.shadow_depth.saturating_sub(1);
6231 }
6232 fn memoize(&mut self, _id: u64, _hash: u64, _r: &dyn Fn(&mut dyn Renderer)) {}
6233 fn snapshot_render_state(&self) -> RenderStateSnapshot {
6234 RenderStateSnapshot {
6237 clip_depth: self.clip_depth,
6238 opacity_depth: self.opacity_depth,
6239 slice_depth: 0,
6240 shadow_depth: self.shadow_depth,
6241 transform_depth: 0,
6242 vnode_depth: 0,
6243 }
6244 }
6245 fn restore_render_state(&mut self, snap: RenderStateSnapshot) {
6246 self.clip_depth = snap.clip_depth;
6247 self.opacity_depth = snap.opacity_depth;
6248 self.shadow_depth = snap.shadow_depth;
6249 }
6250 fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {}
6251 fn set_camera_3d(&mut self, _camera: &Camera3D) {}
6252 fn push_transform_3d(&mut self, _transform: &Transform3D) {}
6253 fn pop_transform_3d(&mut self) {}
6254 }
6255
6256 impl ElapsedTime for TrackingRenderer {
6257 fn elapsed_time(&self) -> f32 {
6258 0.0
6259 }
6260 fn delta_time(&self) -> f32 {
6261 0.0
6262 }
6263 }
6264
6265 struct StackPushingPanicView;
6269
6270 impl View for StackPushingPanicView {
6271 type Body = Never;
6272 fn body(self) -> Never {
6273 unreachable!()
6274 }
6275 fn render(&self, renderer: &mut dyn Renderer, _rect: Rect) {
6276 renderer.push_clip_rect(Rect::new(0.0, 0.0, 50.0, 50.0));
6277 renderer.push_opacity(0.5);
6278 renderer.push_shadow(2.0, [0.0, 0.0, 0.0, 0.5], [0.0, 0.0]);
6279 panic!("intentional stack-pushing panic");
6280 }
6281 }
6282
6283 #[test]
6284 fn error_boundary_restores_renderer_state_on_panic() {
6285 let boundary = ErrorBoundary::new(StackPushingPanicView);
6289 let mut renderer = TrackingRenderer::new();
6290
6291 let snap_before = renderer.snapshot_render_state();
6293 assert_eq!(snap_before.clip_depth, 0);
6294 assert_eq!(snap_before.opacity_depth, 0);
6295 assert_eq!(snap_before.shadow_depth, 0);
6296
6297 boundary.render(&mut renderer, TEST_RECT);
6299
6300 assert!(boundary.has_error(), "should have caught the panic");
6302 let snap_after = renderer.snapshot_render_state();
6303 assert_eq!(
6304 snap_after.clip_depth, 0,
6305 "clip stack should be restored to empty after panic"
6306 );
6307 assert_eq!(
6308 snap_after.opacity_depth, 0,
6309 "opacity stack should be restored to empty after panic"
6310 );
6311 assert_eq!(
6312 snap_after.shadow_depth, 0,
6313 "shadow stack should be restored to empty after panic"
6314 );
6315 }
6316
6317 #[test]
6318 fn render_state_snapshot_default_is_zeroed() {
6319 let snap = RenderStateSnapshot::default();
6322 assert_eq!(snap.clip_depth, 0);
6323 assert_eq!(snap.opacity_depth, 0);
6324 assert_eq!(snap.slice_depth, 0);
6325 assert_eq!(snap.shadow_depth, 0);
6326 assert_eq!(snap.transform_depth, 0);
6327 assert_eq!(snap.vnode_depth, 0);
6328 }
6329
6330 #[test]
6331 fn render_state_snapshot_round_trip() {
6332 let snap = RenderStateSnapshot {
6333 clip_depth: 3,
6334 opacity_depth: 2,
6335 slice_depth: 1,
6336 shadow_depth: 0,
6337 transform_depth: 4,
6338 vnode_depth: 5,
6339 };
6340 let copied = snap;
6341 assert_eq!(copied, snap);
6342 }
6343}
6344
6345use std::cell::RefCell;
6357
6358thread_local! {
6359 static THEME_CONTEXT: RefCell<Option<color::SemanticColors>> = const { RefCell::new(None) };
6361}
6362
6363pub fn set_current_theme(colors: color::SemanticColors) {
6372 THEME_CONTEXT.with(|cell| {
6373 *cell.borrow_mut() = Some(colors);
6374 });
6375}
6376
6377pub fn clear_current_theme() {
6379 THEME_CONTEXT.with(|cell| {
6380 *cell.borrow_mut() = None;
6381 });
6382}
6383
6384pub fn use_theme() -> color::SemanticColors {
6399 THEME_CONTEXT.with(|cell| {
6400 cell.borrow()
6401 .clone()
6402 .unwrap_or_else(color::SemanticColors::dark)
6403 })
6404}
6405
6406pub mod color {
6415 use super::Color;
6416
6417 #[derive(Debug, Clone)]
6434 pub struct SemanticColors {
6435 pub primary: Color,
6437 pub secondary: Color,
6439 pub accent: Color,
6441 pub background: Color,
6443 pub surface: Color,
6445 pub error: Color,
6447 pub warning: Color,
6449 pub success: Color,
6451 pub text: Color,
6453 pub text_dim: Color,
6455 }
6456
6457 impl SemanticColors {
6458 pub fn dark() -> Self {
6460 Self {
6461 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), }
6472 }
6473
6474 pub fn light() -> Self {
6476 Self {
6477 primary: Color::new(0.35, 0.30, 0.70, 1.0),
6478 secondary: Color::new(0.30, 0.50, 0.30, 1.0),
6479 accent: Color::new(0.30, 0.35, 0.75, 1.0),
6480 background: Color::new(0.97, 0.97, 0.98, 1.0),
6481 surface: Color::new(0.93, 0.93, 0.95, 1.0),
6482 error: Color::new(0.75, 0.15, 0.15, 1.0),
6483 warning: Color::new(0.80, 0.60, 0.0, 1.0),
6484 success: Color::new(0.15, 0.65, 0.30, 1.0),
6485 text: Color::new(0.08, 0.08, 0.10, 1.0),
6486 text_dim: Color::new(0.40, 0.40, 0.45, 1.0),
6487 }
6488 }
6489
6490 pub fn accent_states(&self) -> InteractiveColorStates {
6495 InteractiveColorStates::from_color(self.accent)
6496 }
6497
6498 pub fn primary_states(&self) -> InteractiveColorStates {
6500 InteractiveColorStates::from_color(self.primary)
6501 }
6502
6503 pub fn error_states(&self) -> InteractiveColorStates {
6505 InteractiveColorStates::from_color(self.error)
6506 }
6507
6508 pub fn success_states(&self) -> InteractiveColorStates {
6510 InteractiveColorStates::from_color(self.success)
6511 }
6512 }
6513
6514 #[derive(Debug, Clone)]
6519 pub struct InteractiveColorStates {
6520 pub default: Color,
6521 pub hover: Color,
6522 pub active: Color,
6523 pub focus: Color,
6524 pub disabled: Color,
6525 pub focus_ring: Color,
6526 }
6527
6528 impl InteractiveColorStates {
6529 pub fn from_color(base: Color) -> Self {
6538 Self {
6539 default: base,
6540 hover: base.lighten(0.15),
6541 active: base.darken(0.15),
6542 focus: base,
6543 disabled: Color::new(base.r, base.g, base.b, base.a * 0.4),
6544 focus_ring: Color::new(base.r, base.g, base.b, base.a * 0.7),
6545 }
6546 }
6547
6548 pub fn color_for(&self, state: InteractiveState) -> Color {
6550 match state {
6551 InteractiveState::Default => self.default,
6552 InteractiveState::Hover => self.hover,
6553 InteractiveState::Active => self.active,
6554 InteractiveState::Focus => self.focus,
6555 InteractiveState::Disabled => self.disabled,
6556 }
6557 }
6558 }
6559
6560 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6562 pub enum InteractiveState {
6563 Default,
6564 Hover,
6565 Active,
6566 Focus,
6567 Disabled,
6568 }
6569}
6570
6571pub fn use_state<T: Clone + Send + Sync + 'static>(
6590 id: u64,
6591 initial: T,
6592) -> (impl Fn() -> T, impl Fn(T)) {
6593 let already_exists = load_system_state().get_component_state::<T>(id).is_some();
6595 if !already_exists {
6596 update_system_state(|s| {
6597 let mut ns = s.clone();
6598 ns.set_component_state(id, initial.clone());
6599 ns
6600 });
6601 }
6602
6603 let getter = move || -> T {
6604 load_system_state()
6605 .get_component_state::<T>(id)
6606 .map(|arc_lock| {
6607 arc_lock
6608 .read()
6609 .ok()
6610 .map(|guard| (*guard).clone())
6611 .unwrap_or_else(|| initial.clone())
6612 })
6613 .unwrap_or_else(|| initial.clone())
6614 };
6615
6616 let setter = {
6617 move |value| {
6618 update_system_state(|s| {
6619 let mut ns = s.clone();
6620 ns.set_component_state(id, value);
6621 ns
6622 });
6623 }
6624 };
6625
6626 (getter, setter)
6627}
6628
6629pub fn use_state_hash(key: &str) -> u64 {
6641 use std::hash::{Hash, Hasher};
6642 let mut s = std::collections::hash_map::DefaultHasher::new();
6643 key.hash(&mut s);
6644 s.finish()
6645}
6646
6647thread_local! {
6657 static ACCESSIBILITY_PREFS: std::cell::RefCell<AccessibilityPreferences> =
6660 std::cell::RefCell::new(AccessibilityPreferences::default());
6661}
6662
6663#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6670pub struct AccessibilityPreferences {
6671 pub reduce_motion: bool,
6673 pub reduce_transparency: bool,
6675 pub increase_contrast: bool,
6677}
6678
6679impl AccessibilityPreferences {
6680 pub fn detect_from_system() -> Self {
6685 #[cfg(target_os = "macos")]
6686 {
6687 let reduce_motion = std::process::Command::new("defaults")
6689 .args(["read", "-g", "com.apple.universalaccess", "reduceMotion"])
6690 .output()
6691 .ok()
6692 .and_then(|o| String::from_utf8(o.stdout).ok())
6693 .map(|s| s.trim() == "1")
6694 .unwrap_or(false);
6695
6696 let reduce_transparency = std::process::Command::new("defaults")
6697 .args([
6698 "read",
6699 "-g",
6700 "com.apple.universalaccess",
6701 "reduceTransparency",
6702 ])
6703 .output()
6704 .ok()
6705 .and_then(|o| String::from_utf8(o.stdout).ok())
6706 .map(|s| s.trim() == "1")
6707 .unwrap_or(false);
6708
6709 let increase_contrast = std::process::Command::new("defaults")
6710 .args([
6711 "read",
6712 "-g",
6713 "com.apple.universalaccess",
6714 "increaseContrast",
6715 ])
6716 .output()
6717 .ok()
6718 .and_then(|o| String::from_utf8(o.stdout).ok())
6719 .map(|s| s.trim() == "1")
6720 .unwrap_or(false);
6721
6722 Self {
6723 reduce_motion,
6724 reduce_transparency,
6725 increase_contrast,
6726 }
6727 }
6728
6729 #[cfg(target_os = "linux")]
6730 {
6731 let reduce_motion = std::env::var("GTK_A11Y")
6733 .map(|v| v.to_lowercase().contains("reduce-motion"))
6734 .unwrap_or(false)
6735 || {
6736 std::process::Command::new("gsettings")
6738 .args([
6739 "get",
6740 "org.gnome.desktop.interface",
6741 "enable-animations",
6742 ])
6743 .output()
6744 .ok()
6745 .and_then(|o| String::from_utf8(o.stdout).ok())
6746 .map(|s| s.trim() == "'false'" || s.trim() == "false")
6747 .unwrap_or(false)
6748 };
6749
6750 let reduce_transparency = false;
6752
6753 let increase_contrast = std::env::var("GTK_THEME")
6755 .map(|v| v.to_lowercase().contains("highcontrast"))
6756 .unwrap_or(false);
6757
6758 Self {
6759 reduce_motion,
6760 reduce_transparency,
6761 increase_contrast,
6762 }
6763 }
6764
6765 #[cfg(target_os = "windows")]
6766 {
6767 use std::process::Command;
6768
6769 fn reg_query(key: &str, value_name: &str) -> Option<String> {
6771 Command::new("reg")
6772 .args(["query", key, "/v", value_name])
6773 .output()
6774 .ok()
6775 .and_then(|o| {
6776 if o.status.success() {
6777 String::from_utf8(o.stdout).ok()
6778 } else {
6779 None
6780 }
6781 })
6782 .and_then(|s| {
6783 s.lines()
6786 .last()?
6787 .split_whitespace()
6788 .last()
6789 .map(String::from)
6790 })
6791 }
6792
6793 let reduce_motion = reg_query(
6795 "HKCU\\Control Panel\\Accessibility\\EffectsAnimationEfficiency",
6796 "EffectsAnimationEfficiency",
6797 )
6798 .map(|v| v == "1")
6799 .unwrap_or(false);
6800
6801 let reduce_transparency = reg_query(
6803 "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
6804 "EnableTransparency",
6805 )
6806 .map(|v| v == "0")
6807 .unwrap_or(false);
6808
6809 let increase_contrast = reg_query(
6811 "HKCU\\Control Panel\\Accessibility\\HighContrast",
6812 "HighContrast",
6813 )
6814 .map(|v| v == "1")
6815 .unwrap_or(false);
6816
6817 Self {
6818 reduce_motion,
6819 reduce_transparency,
6820 increase_contrast,
6821 }
6822 }
6823
6824 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
6825 {
6826 Self::default()
6827 }
6828 }
6829
6830 pub fn min_alpha(&self, requested: f32) -> f32 {
6832 if self.increase_contrast {
6833 requested.max(0.5)
6834 } else {
6835 requested
6836 }
6837 }
6838
6839 pub fn should_disable_glass(&self) -> bool {
6841 self.reduce_transparency
6842 }
6843
6844 pub fn should_reduce_motion(&self) -> bool {
6846 self.reduce_motion
6847 }
6848
6849 pub fn should_increase_contrast(&self) -> bool {
6851 self.increase_contrast
6852 }
6853}
6854
6855pub fn accessibility_preferences() -> AccessibilityPreferences {
6857 ACCESSIBILITY_PREFS.with(|p| *p.borrow())
6858}
6859
6860pub fn set_accessibility_preferences(prefs: AccessibilityPreferences) {
6865 ACCESSIBILITY_PREFS.with(|p| {
6866 *p.borrow_mut() = prefs;
6867 });
6868}
6869
6870pub trait ClipboardProvider: Send + Sync {
6879 fn read_text(&self) -> Option<String>;
6881 fn write_text(&self, text: &str);
6883}
6884
6885#[cfg(not(target_arch = "wasm32"))]
6889pub struct SystemClipboard;
6890
6891#[cfg(not(target_arch = "wasm32"))]
6892impl ClipboardProvider for SystemClipboard {
6893 fn read_text(&self) -> Option<String> {
6894 use std::process::Command;
6895 Command::new("pbpaste")
6897 .output()
6898 .ok()
6899 .and_then(|o| String::from_utf8(o.stdout).ok())
6900 }
6901
6902 fn write_text(&self, text: &str) {
6903 use std::process::Command;
6904 if let Ok(mut child) = Command::new("pbcopy")
6906 .stdin(std::process::Stdio::piped())
6907 .spawn()
6908 {
6909 if let Some(stdin) = child.stdin.as_mut() {
6910 use std::io::Write;
6911 let _ = stdin.write_all(text.as_bytes());
6912 }
6913 let _ = child.wait();
6914 }
6915 }
6916}
6917
6918#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6924pub enum TextDirection {
6925 Forward,
6926 Backward,
6927 Up,
6928 Down,
6929 LineStart,
6930 LineEnd,
6931 WordForward,
6932 WordBackward,
6933}
6934
6935#[derive(Debug, Clone, Default)]
6940pub struct TextInputState {
6941 pub text: String,
6943 pub cursor_pos: usize,
6945 pub selection_anchor: Option<usize>,
6948 pub focused: bool,
6950 pub caret_visible: bool,
6952 pub last_edit_time: f32,
6954}
6955
6956impl TextInputState {
6957 pub fn new(text: impl Into<String>) -> Self {
6959 let text = text.into();
6960 let cursor_pos = text.len();
6961 Self {
6962 text,
6963 cursor_pos,
6964 selection_anchor: None,
6965 focused: false,
6966 caret_visible: true,
6967 last_edit_time: 0.0,
6968 }
6969 }
6970
6971 pub fn selection_range(&self) -> Option<(usize, usize)> {
6974 self.selection_anchor.map(|anchor| {
6975 if anchor <= self.cursor_pos {
6976 (anchor, self.cursor_pos)
6977 } else {
6978 (self.cursor_pos, anchor)
6979 }
6980 })
6981 }
6982
6983 pub fn selected_text(&self) -> String {
6985 self.selection_range()
6986 .map(|(start, end)| self.text[start..end].to_string())
6987 .unwrap_or_default()
6988 }
6989
6990 pub fn insert(&mut self, new_text: &str) {
6992 if let Some((start, end)) = self.selection_range() {
6993 self.text.replace_range(start..end, new_text);
6994 self.cursor_pos = start + new_text.len();
6995 } else {
6996 self.text.insert_str(self.cursor_pos, new_text);
6997 self.cursor_pos += new_text.len();
6998 }
6999 self.selection_anchor = None;
7000 }
7001
7002 pub fn delete(&mut self, backward: bool, count: usize) -> String {
7005 if let Some((start, end)) = self.selection_range() {
7006 let deleted = self.text[start..end].to_string();
7007 self.text.replace_range(start..end, "");
7008 self.cursor_pos = start;
7009 self.selection_anchor = None;
7010 return deleted;
7011 }
7012
7013 if backward && self.cursor_pos > 0 {
7014 let start = self.cursor_pos.saturating_sub(count);
7015 let deleted = self.text[start..self.cursor_pos].to_string();
7016 self.text.replace_range(start..self.cursor_pos, "");
7017 self.cursor_pos = start;
7018 deleted
7019 } else if !backward && self.cursor_pos < self.text.len() {
7020 let end = (self.cursor_pos + count).min(self.text.len());
7021 let deleted = self.text[self.cursor_pos..end].to_string();
7022 self.text.replace_range(self.cursor_pos..end, "");
7023 deleted
7024 } else {
7025 String::new()
7026 }
7027 }
7028
7029 pub fn move_cursor(&mut self, direction: TextDirection, extend_selection: bool) {
7031 if !extend_selection {
7032 self.selection_anchor = None;
7033 } else if self.selection_anchor.is_none() {
7034 self.selection_anchor = Some(self.cursor_pos);
7035 }
7036
7037 match direction {
7038 TextDirection::Forward if self.cursor_pos < self.text.len() => {
7039 let next = self.text[self.cursor_pos..]
7041 .char_indices()
7042 .nth(1)
7043 .map(|(i, _)| self.cursor_pos + i)
7044 .unwrap_or(self.text.len());
7045 self.cursor_pos = next;
7046 }
7047 TextDirection::Backward if self.cursor_pos > 0 => {
7048 let prev = self.text[..self.cursor_pos]
7049 .char_indices()
7050 .next_back()
7051 .map(|(i, _)| i)
7052 .unwrap_or(0);
7053 self.cursor_pos = prev;
7054 }
7055 TextDirection::LineStart => {
7056 self.cursor_pos = 0;
7057 }
7058 TextDirection::LineEnd => {
7059 self.cursor_pos = self.text.len();
7060 }
7061 TextDirection::WordForward => {
7062 let rest = &self.text[self.cursor_pos..];
7064 let after_word = rest
7066 .char_indices()
7067 .find(|(_, c)| !c.is_alphanumeric())
7068 .map(|(i, _)| i)
7069 .unwrap_or(rest.len());
7070 let after_space = rest[after_word..]
7072 .char_indices()
7073 .find(|(_, c)| !c.is_whitespace())
7074 .map(|(i, _)| after_word + i)
7075 .unwrap_or(rest.len());
7076 self.cursor_pos = (self.cursor_pos + after_space).min(self.text.len());
7077 }
7078 TextDirection::WordBackward => {
7079 let before = &self.text[..self.cursor_pos];
7080 let before_word = before
7082 .char_indices()
7083 .rev()
7084 .find(|(_, c)| !c.is_whitespace())
7085 .map(|(i, _)| i)
7086 .unwrap_or(0);
7087 let word_start = before[..before_word]
7089 .char_indices()
7090 .rev()
7091 .find(|(_, c)| !c.is_alphanumeric())
7092 .map(|(i, _)| i)
7093 .unwrap_or(0);
7094 self.cursor_pos = word_start;
7095 }
7096 _ => {} }
7098
7099 if !extend_selection {
7100 self.selection_anchor = None;
7101 }
7102 }
7103
7104 pub fn select_all(&mut self) {
7106 self.cursor_pos = self.text.len();
7107 self.selection_anchor = Some(0);
7108 }
7109
7110 pub fn cursor_byte_pos(&self) -> usize {
7112 self.cursor_pos
7113 }
7114}
7115
7116#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7118pub struct NotificationAction {
7119 pub id: String,
7121 pub title: String,
7123 pub is_destructive: bool,
7125}
7126
7127#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7129pub enum NotificationPriority {
7130 Passive,
7132 #[default]
7134 Active,
7135 TimeSensitive,
7137}
7138
7139#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
7141pub struct Notification {
7142 pub id: String,
7144 pub app_name: Option<String>,
7146 pub title: String,
7148 pub body: String,
7150 pub icon: Option<String>,
7152 pub sound: Option<String>,
7154 pub actions: Vec<NotificationAction>,
7156 pub timeout: Option<f32>,
7158 pub priority: NotificationPriority,
7160 pub timestamp: f32,
7162 pub dismissed: bool,
7164}
7165
7166#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, thiserror::Error)]
7168pub enum NotificationError {
7169 #[error("Notification permission denied")]
7171 PermissionDenied,
7172 #[error("Failed to post notification")]
7174 PostFailed,
7175}
7176
7177#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7179pub enum NotificationPermission {
7180 Granted,
7182 Denied,
7184 #[default]
7186 NotDetermined,
7187}
7188
7189pub trait NotificationHandler: Send + Sync {
7191 fn show(&self, notification: Notification) -> Result<(), NotificationError>;
7193 fn dismiss(&self, id: &str) -> Result<(), NotificationError>;
7195 fn request_permission(&self) -> NotificationPermission;
7197}
7198
7199static NEXT_NOTIFICATION_ID: std::sync::atomic::AtomicUsize =
7200 std::sync::atomic::AtomicUsize::new(1);
7201
7202#[derive(Clone, Copy, Debug, Default)]
7204pub struct DefaultNotificationHandler;
7205
7206impl NotificationHandler for DefaultNotificationHandler {
7207 fn show(&self, notification: Notification) -> Result<(), NotificationError> {
7209 let mut notif = notification;
7210 if notif.id.is_empty() {
7211 let id = NEXT_NOTIFICATION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7212 notif.id = format!("notif_{}", id);
7213 }
7214 update_system_state(|state| {
7215 let mut new_state = state.clone();
7216 new_state.notifications.push(notif.clone());
7217 new_state
7218 });
7219 Ok(())
7220 }
7221
7222 fn dismiss(&self, id: &str) -> Result<(), NotificationError> {
7224 update_system_state(|state| {
7225 let mut new_state = state.clone();
7226 for notif in &mut new_state.notifications {
7227 if notif.id == id {
7228 notif.dismissed = true;
7229 }
7230 }
7231 new_state
7232 });
7233 Ok(())
7234 }
7235
7236 fn request_permission(&self) -> NotificationPermission {
7238 NotificationPermission::Granted
7239 }
7240}
7241
7242static NOTIFICATION_HANDLER: once_cell::sync::OnceCell<std::sync::Arc<dyn NotificationHandler>> =
7243 once_cell::sync::OnceCell::new();
7244
7245pub fn set_notification_handler(handler: std::sync::Arc<dyn NotificationHandler>) {
7247 let _ = NOTIFICATION_HANDLER.set(handler);
7248}
7249
7250pub fn get_notification_handler() -> std::sync::Arc<dyn NotificationHandler> {
7252 NOTIFICATION_HANDLER
7253 .get_or_init(|| std::sync::Arc::new(DefaultNotificationHandler))
7254 .clone()
7255}
7256
7257#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7259pub struct FileFilter {
7260 pub name: String,
7262 pub extensions: Vec<String>,
7264}
7265
7266#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
7268pub enum FileDialogMode {
7269 #[default]
7271 OpenFile,
7272 OpenDirectory,
7274 SaveFile,
7276}
7277
7278#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
7280pub struct FileDialog {
7281 pub title: String,
7283 pub default_path: Option<String>,
7285 pub filters: Vec<FileFilter>,
7287 pub mode: FileDialogMode,
7289 pub allow_multiple: bool,
7291}
7292
7293#[derive(Debug, thiserror::Error)]
7295pub enum FileDialogError {
7296 #[error("File dialog cancelled")]
7298 Cancelled,
7299 #[error("I/O error: {0}")]
7301 Io(#[from] std::io::Error),
7302 #[error("Platform error: {0}")]
7304 Platform(String),
7305}
7306
7307impl FileDialog {
7308 pub fn new(mode: FileDialogMode) -> Self {
7310 Self {
7311 mode,
7312 ..Default::default()
7313 }
7314 }
7315
7316 pub fn title(mut self, title: impl Into<String>) -> Self {
7318 self.title = title.into();
7319 self
7320 }
7321
7322 pub fn add_filter(mut self, name: &str, extensions: &[&str]) -> Self {
7324 self.filters.push(FileFilter {
7325 name: name.to_string(),
7326 extensions: extensions.iter().map(|s| s.to_string()).collect(),
7327 });
7328 self
7329 }
7330
7331 pub fn default_path(mut self, path: impl Into<String>) -> Self {
7333 self.default_path = Some(path.into());
7334 self
7335 }
7336
7337 pub fn allow_multiple(mut self, allow: bool) -> Self {
7339 self.allow_multiple = allow;
7340 self
7341 }
7342}
7343
7344#[cfg(not(target_arch = "wasm32"))]
7345impl FileDialog {
7346 pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7348 let mut dialog = rfd::FileDialog::new();
7349 dialog = dialog.set_title(&self.title);
7350 if let Some(path) = &self.default_path {
7351 dialog = dialog.set_directory(path);
7352 }
7353 for filter in &self.filters {
7354 let refs: Vec<&str> = filter.extensions.iter().map(|s| s.as_str()).collect();
7355 dialog = dialog.add_filter(&filter.name, &refs);
7356 }
7357
7358 match self.mode {
7359 FileDialogMode::OpenFile => {
7360 if self.allow_multiple {
7361 dialog.pick_files().ok_or(FileDialogError::Cancelled)
7362 } else {
7363 Ok(dialog.pick_file().into_iter().collect())
7364 }
7365 }
7366 FileDialogMode::OpenDirectory => Ok(dialog.pick_folder().into_iter().collect()),
7367 FileDialogMode::SaveFile => Ok(dialog.save_file().into_iter().collect()),
7368 }
7369 }
7370
7371 pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7373 let results = self.pick()?;
7374 Ok(results.into_iter().next())
7375 }
7376}
7377
7378#[cfg(target_arch = "wasm32")]
7379impl FileDialog {
7380 pub fn pick(self) -> Result<Vec<std::path::PathBuf>, FileDialogError> {
7382 Err(FileDialogError::Platform(
7383 "FileDialog is not supported synchronously on WebAssembly".to_string(),
7384 ))
7385 }
7386
7387 pub fn pick_single(self) -> Result<Option<std::path::PathBuf>, FileDialogError> {
7389 Err(FileDialogError::Platform(
7390 "FileDialog is not supported synchronously on WebAssembly".to_string(),
7391 ))
7392 }
7393}
7394
7395#[derive(Debug, thiserror::Error)]
7397pub enum DocumentError {
7398 #[error("I/O error: {0}")]
7400 Io(#[from] std::io::Error),
7401 #[error("Parse error: {0}")]
7403 Parse(String),
7404 #[error("Serialization error: {0}")]
7406 Serialize(String),
7407}
7408
7409pub trait Document: Send + Sync {
7411 fn read_from(path: &std::path::Path) -> Result<Self, DocumentError>
7413 where
7414 Self: Sized;
7415
7416 fn write_to(&self, path: &std::path::Path) -> Result<(), DocumentError>;
7418
7419 fn is_dirty(&self) -> bool;
7421
7422 fn mark_clean(&mut self);
7424}
7425
7426pub struct AutoSaveManager {
7428 pub interval: f32,
7430 pub timer: f32,
7432 pub documents: Vec<(std::path::PathBuf, Box<dyn Document>)>,
7434}
7435
7436impl AutoSaveManager {
7437 pub fn new(interval: f32) -> Self {
7439 Self {
7440 interval,
7441 timer: 0.0,
7442 documents: Vec::new(),
7443 }
7444 }
7445
7446 pub fn register(&mut self, path: std::path::PathBuf, doc: Box<dyn Document>) {
7448 self.documents.push((path, doc));
7449 }
7450
7451 pub fn tick(&mut self, dt: f32) {
7453 self.timer += dt;
7454 if self.timer >= self.interval {
7455 self.timer = 0.0;
7456 for (path, doc) in &mut self.documents {
7457 if doc.is_dirty() {
7458 match doc.write_to(path) {
7459 Ok(()) => {
7460 doc.mark_clean();
7461 log::info!("[AutoSaveManager] Auto-saved document to {:?}", path);
7462 }
7463 Err(e) => {
7464 log::error!(
7465 "[AutoSaveManager] Failed to auto-save document to {:?}: {:?}",
7466 path,
7467 e
7468 );
7469 }
7470 }
7471 }
7472 }
7473 }
7474 }
7475}
7476
7477#[derive(Debug, Clone, PartialEq, Eq, Default)]
7485pub struct Modifiers {
7486 pub cmd: bool,
7488 pub shift: bool,
7490 pub alt: bool,
7492 pub ctrl: bool,
7494}
7495
7496#[derive(Debug, Clone)]
7498pub struct KeyboardShortcut {
7499 pub key: String,
7501 pub modifiers: Modifiers,
7503}
7504
7505impl KeyboardShortcut {
7506 pub fn cmd(key: impl Into<String>) -> Self {
7508 Self {
7509 key: key.into(),
7510 modifiers: Modifiers {
7511 cmd: true,
7512 ..Default::default()
7513 },
7514 }
7515 }
7516
7517 pub fn cmd_shift(key: impl Into<String>) -> Self {
7519 Self {
7520 key: key.into(),
7521 modifiers: Modifiers {
7522 cmd: true,
7523 shift: true,
7524 ..Default::default()
7525 },
7526 }
7527 }
7528}
7529
7530pub enum MenuItem {
7536 Action {
7538 label: String,
7539 shortcut: Option<KeyboardShortcut>,
7540 action: std::sync::Arc<dyn Fn() + Send + Sync>,
7541 enabled: bool,
7542 },
7543 Submenu { label: String, items: Vec<MenuItem> },
7545 Separator,
7547}
7548
7549impl std::fmt::Debug for MenuItem {
7550 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7551 match self {
7552 Self::Action { label, enabled, .. } => f
7553 .debug_struct("Action")
7554 .field("label", label)
7555 .field("enabled", enabled)
7556 .finish(),
7557 Self::Submenu { label, items } => f
7558 .debug_struct("Submenu")
7559 .field("label", label)
7560 .field("items", items)
7561 .finish(),
7562 Self::Separator => write!(f, "Separator"),
7563 }
7564 }
7565}
7566
7567pub struct MenuBar {
7572 pub items: Vec<MenuItem>,
7574}
7575
7576impl MenuBar {
7577 pub fn new() -> Self {
7579 Self { items: Vec::new() }
7580 }
7581
7582 pub fn add_item(&mut self, item: MenuItem) {
7584 self.items.push(item);
7585 }
7586
7587 #[allow(clippy::too_many_arguments)]
7599 pub fn standard(
7600 new_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7601 open_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7602 save_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7603 close_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7604 quit_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7605 undo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7606 redo_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7607 cut_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7608 copy_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7609 paste_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7610 select_all_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7611 find_fn: std::sync::Arc<dyn Fn() + Send + Sync>,
7612 ) -> Self {
7613 let mut bar = Self::new();
7614
7615 bar.add_item(MenuItem::Submenu {
7617 label: "File".to_string(),
7618 items: vec![
7619 MenuItem::Action {
7620 label: "New".to_string(),
7621 shortcut: Some(KeyboardShortcut::cmd("n")),
7622 action: new_fn,
7623 enabled: true,
7624 },
7625 MenuItem::Action {
7626 label: "Open…".to_string(),
7627 shortcut: Some(KeyboardShortcut::cmd("o")),
7628 action: open_fn,
7629 enabled: true,
7630 },
7631 MenuItem::Separator,
7632 MenuItem::Action {
7633 label: "Save".to_string(),
7634 shortcut: Some(KeyboardShortcut::cmd("s")),
7635 action: save_fn,
7636 enabled: true,
7637 },
7638 MenuItem::Separator,
7639 MenuItem::Action {
7640 label: "Close".to_string(),
7641 shortcut: Some(KeyboardShortcut::cmd("w")),
7642 action: close_fn,
7643 enabled: true,
7644 },
7645 MenuItem::Separator,
7646 MenuItem::Action {
7647 label: "Quit".to_string(),
7648 shortcut: Some(KeyboardShortcut::cmd("q")),
7649 action: quit_fn,
7650 enabled: true,
7651 },
7652 ],
7653 });
7654
7655 bar.add_item(MenuItem::Submenu {
7657 label: "Edit".to_string(),
7658 items: vec![
7659 MenuItem::Action {
7660 label: "Undo".to_string(),
7661 shortcut: Some(KeyboardShortcut::cmd("z")),
7662 action: undo_fn,
7663 enabled: true,
7664 },
7665 MenuItem::Action {
7666 label: "Redo".to_string(),
7667 shortcut: Some(KeyboardShortcut::cmd_shift("z")),
7668 action: redo_fn,
7669 enabled: true,
7670 },
7671 MenuItem::Separator,
7672 MenuItem::Action {
7673 label: "Cut".to_string(),
7674 shortcut: Some(KeyboardShortcut::cmd("x")),
7675 action: cut_fn,
7676 enabled: true,
7677 },
7678 MenuItem::Action {
7679 label: "Copy".to_string(),
7680 shortcut: Some(KeyboardShortcut::cmd("c")),
7681 action: copy_fn,
7682 enabled: true,
7683 },
7684 MenuItem::Action {
7685 label: "Paste".to_string(),
7686 shortcut: Some(KeyboardShortcut::cmd("v")),
7687 action: paste_fn,
7688 enabled: true,
7689 },
7690 MenuItem::Separator,
7691 MenuItem::Action {
7692 label: "Select All".to_string(),
7693 shortcut: Some(KeyboardShortcut::cmd("a")),
7694 action: select_all_fn,
7695 enabled: true,
7696 },
7697 MenuItem::Separator,
7698 MenuItem::Action {
7699 label: "Find…".to_string(),
7700 shortcut: Some(KeyboardShortcut::cmd("f")),
7701 action: find_fn,
7702 enabled: true,
7703 },
7704 ],
7705 });
7706
7707 let noop: std::sync::Arc<dyn Fn() + Send + Sync> = std::sync::Arc::new(|| {});
7711 bar.add_item(MenuItem::Submenu {
7712 label: "View".to_string(),
7713 items: vec![
7714 MenuItem::Action {
7715 label: "Zoom In".to_string(),
7716 shortcut: Some(KeyboardShortcut::cmd("=")),
7717 action: noop.clone(),
7718 enabled: true,
7719 },
7720 MenuItem::Action {
7721 label: "Zoom Out".to_string(),
7722 shortcut: Some(KeyboardShortcut::cmd("-")),
7723 action: noop.clone(),
7724 enabled: true,
7725 },
7726 MenuItem::Separator,
7727 MenuItem::Action {
7728 label: "Toggle Fullscreen".to_string(),
7729 shortcut: Some(KeyboardShortcut {
7730 key: "f".to_string(),
7731 modifiers: Modifiers {
7732 ctrl: true,
7733 ..Default::default()
7734 },
7735 }),
7736 action: noop.clone(),
7737 enabled: true,
7738 },
7739 ],
7740 });
7741
7742 bar.add_item(MenuItem::Submenu {
7744 label: "Window".to_string(),
7745 items: vec![
7746 MenuItem::Action {
7747 label: "Minimize".to_string(),
7748 shortcut: Some(KeyboardShortcut::cmd("m")),
7749 action: noop.clone(),
7750 enabled: true,
7751 },
7752 MenuItem::Action {
7753 label: "Zoom".to_string(),
7754 shortcut: None,
7755 action: noop.clone(),
7756 enabled: true,
7757 },
7758 MenuItem::Separator,
7759 MenuItem::Action {
7760 label: "Bring All to Front".to_string(),
7761 shortcut: None,
7762 action: noop.clone(),
7763 enabled: true,
7764 },
7765 ],
7766 });
7767
7768 bar.add_item(MenuItem::Submenu {
7770 label: "Help".to_string(),
7771 items: vec![MenuItem::Action {
7772 label: "Search Help".to_string(),
7773 shortcut: None,
7774 action: noop,
7775 enabled: true,
7776 }],
7777 });
7778
7779 bar
7780 }
7781}
7782
7783impl Default for MenuBar {
7784 fn default() -> Self {
7785 Self::new()
7786 }
7787}
7788
7789use std::sync::RwLock;
7795
7796#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7798pub enum Direction {
7799 #[default]
7800 LTR,
7801 RTL,
7802 Auto,
7803}
7804
7805impl Direction {
7806 pub fn is_rtl(self) -> bool {
7807 matches!(self, Direction::RTL)
7808 }
7809}
7810#[derive(Clone, Debug)]
7811pub struct L10nBundle {
7812 pub locale: String,
7813 pub strings: HashMap<String, String>,
7814 pub is_rtl: bool,
7815}
7816
7817impl L10nBundle {
7818 pub fn new(locale: impl Into<String>) -> Self {
7819 Self {
7820 locale: locale.into(),
7821 strings: HashMap::new(),
7822 is_rtl: false,
7823 }
7824 }
7825
7826 pub fn add(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
7827 self.strings.insert(key.into(), value.into());
7828 self
7829 }
7830
7831 pub fn from_strings_format(locale: impl Into<String>, input: &str) -> Self {
7832 let mut bundle = Self::new(locale);
7833 for line in input.lines() {
7834 let line = line.trim();
7835 if line.is_empty() || line.starts_with("//") {
7836 continue;
7837 }
7838 if let Some(eq_pos) = line.find(" = ") {
7839 let key = line[..eq_pos].trim_matches('"').to_string();
7840 let val = line[eq_pos + 3..]
7841 .trim_end_matches(';')
7842 .trim_matches('"')
7843 .to_string();
7844 bundle.strings.insert(key, val);
7845 }
7846 }
7847 bundle
7848 }
7849 pub fn t(&self, key: &str) -> String {
7851 self.strings
7852 .get(key)
7853 .map(|s| s.to_string())
7854 .unwrap_or_else(|| key.to_string())
7855 }
7856
7857 pub fn tf(&self, key: &str, args: &[&str]) -> String {
7859 let mut result = self.t(key);
7860 for (i, arg) in args.iter().enumerate() {
7861 result = result.replace(&format!("{{{}}}", i), arg);
7862 }
7863 result
7864 }
7865}
7866
7867pub struct L10n {
7869 bundles: HashMap<String, L10nBundle>,
7870 current: String,
7871}
7872
7873impl L10n {
7874 pub fn new(default_locale: &str) -> Self {
7875 Self {
7876 bundles: HashMap::new(),
7877 current: default_locale.to_string(),
7878 }
7879 }
7880
7881 pub fn add_bundle(&mut self, bundle: L10nBundle) {
7882 self.bundles.insert(bundle.locale.clone(), bundle);
7883 }
7884
7885 pub fn set_locale(&mut self, locale: &str) {
7886 self.current = locale.to_string();
7887 }
7888 pub fn current_locale(&self) -> &str {
7889 &self.current
7890 }
7891
7892 pub fn is_rtl(&self) -> bool {
7893 self.bundles
7894 .get(self.current.as_str())
7895 .map(|b| b.is_rtl)
7896 .unwrap_or(false)
7897 }
7898
7899 pub fn t(&self, key: &str) -> String {
7900 self.bundles
7901 .get(self.current.as_str())
7902 .map(|b| b.t(key))
7903 .unwrap_or_else(|| key.to_string())
7904 }
7905
7906 pub fn tf(&self, key: &str, args: &[&str]) -> String {
7907 let mut result = self.t(key);
7908 for (i, arg) in args.iter().enumerate() {
7909 result = result.replace(&format!("{{{}}}", i), arg);
7910 }
7911 result
7912 }
7913
7914 pub fn direction(&self) -> Direction {
7915 if self.is_rtl() {
7916 Direction::RTL
7917 } else {
7918 Direction::LTR
7919 }
7920 }
7921}
7922
7923static L10N: once_cell::sync::Lazy<Arc<RwLock<L10n>>> =
7924 once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(L10n::new("en"))));
7925
7926pub fn init_l10n(l10n: L10n) {
7927 if let Ok(mut guard) = L10N.write() {
7928 *guard = l10n;
7929 }
7930}
7931
7932pub fn l10n() -> Arc<RwLock<L10n>> {
7933 L10N.clone()
7934}
7935
7936pub fn t(key: &str) -> String {
7937 L10N.read()
7938 .map(|g| g.t(key).to_string())
7939 .unwrap_or_else(|_| key.to_string())
7940}
7941
7942pub fn tf(key: &str, args: &[&str]) -> String {
7943 L10N.read()
7944 .map(|g| g.tf(key, args))
7945 .unwrap_or_else(|_| key.to_string())
7946}
7947
7948pub fn set_locale(locale: &str) {
7949 if let Ok(mut guard) = L10N.write() {
7950 guard.set_locale(locale);
7951 }
7952}
7953
7954pub fn current_locale() -> String {
7955 L10N.read()
7956 .map(|g| g.current_locale().to_string())
7957 .unwrap_or_else(|_| "en".to_string())
7958}
7959
7960pub fn is_rtl() -> bool {
7961 L10N.read().map(|g| g.is_rtl()).unwrap_or(false)
7962}
7963
7964#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
7976pub enum SystemTheme {
7977 #[default]
7979 Dark,
7980 Light,
7982}
7983
7984pub fn detect_system_theme() -> SystemTheme {
7994 std::env::var("CVKG_THEME")
7995 .ok()
7996 .and_then(|v| match v.as_str() {
7997 "light" => Some(SystemTheme::Light),
7998 "dark" => Some(SystemTheme::Dark),
7999 _ => None,
8000 })
8001 .unwrap_or(SystemTheme::Dark)
8002}
8003
8004pub mod audio_haptic;
8010pub use audio_haptic::{
8011 AudioEngine, HapticEngine, HapticIntensity, NullAudioEngine, NullHapticEngine, haptic_error,
8012 haptic_impact, haptic_selection, haptic_success, play_sound, set_audio_engine,
8013 set_haptic_engine, sounds,
8014};
8015
8016pub mod parallax;
8021pub use parallax::{DisplayEnvironment, ParallaxModifier, PerformanceContract, Tier3Fallback};
8022
8023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8042pub struct KvasirId(pub u64);
8043
8044impl KvasirId {
8045 pub const NULL: KvasirId = KvasirId(0);
8047
8048 pub fn new() -> Self {
8054 use std::sync::atomic::{AtomicU64, Ordering};
8055 static COUNTER: AtomicU64 = AtomicU64::new(1);
8056 KvasirId(COUNTER.fetch_add(1, Ordering::Relaxed))
8057 }
8058
8059 pub fn is_null(self) -> bool {
8061 self.0 == 0
8062 }
8063}
8064
8065impl std::fmt::Display for KvasirId {
8066 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8067 write!(f, "KvasirId({})", self.0)
8068 }
8069}
8070
8071impl From<u64> for KvasirId {
8088 fn from(value: u64) -> Self {
8089 KvasirId(value)
8090 }
8091}
8092
8093impl From<KvasirId> for u64 {
8094 fn from(id: KvasirId) -> Self {
8095 id.0
8096 }
8097}
8098
8099#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8122pub struct DirtyFlags(pub u8);
8123
8124impl DirtyFlags {
8125 pub const CLEAN: DirtyFlags = DirtyFlags(0b0000_0000);
8127 pub const STATE: DirtyFlags = DirtyFlags(0b0000_1111);
8129 pub const LAYOUT: DirtyFlags = DirtyFlags(0b0000_0111);
8131 pub const PAINT: DirtyFlags = DirtyFlags(0b0000_0011);
8133 pub const COMPOSITE: DirtyFlags = DirtyFlags(0b0000_0001);
8135 pub const ALL: DirtyFlags = DirtyFlags(0b0000_1111);
8137
8138 #[inline]
8140 pub fn is_dirty(self) -> bool {
8141 self.0 != 0
8142 }
8143
8144 #[inline]
8146 pub fn needs_composite(self) -> bool {
8147 self.0 & 0b0000_0001 != 0
8148 }
8149
8150 #[inline]
8152 pub fn needs_paint(self) -> bool {
8153 self.0 & 0b0000_0010 != 0
8154 }
8155
8156 #[inline]
8158 pub fn needs_layout(self) -> bool {
8159 self.0 & 0b0000_0100 != 0
8160 }
8161
8162 #[inline]
8164 pub fn needs_state(self) -> bool {
8165 self.0 & 0b0000_1000 != 0
8166 }
8167
8168 #[inline]
8170 pub fn merge(self, other: DirtyFlags) -> DirtyFlags {
8171 DirtyFlags(self.0 | other.0)
8172 }
8173
8174 #[inline]
8176 pub fn clear(self) -> DirtyFlags {
8177 DirtyFlags::CLEAN
8178 }
8179}
8180
8181impl std::ops::BitOr for DirtyFlags {
8182 type Output = DirtyFlags;
8183 fn bitor(self, rhs: DirtyFlags) -> DirtyFlags {
8184 DirtyFlags(self.0 | rhs.0)
8185 }
8186}
8187
8188impl std::ops::BitOrAssign for DirtyFlags {
8189 fn bitor_assign(&mut self, rhs: DirtyFlags) {
8190 self.0 |= rhs.0;
8191 }
8192}
8193
8194impl std::ops::BitAnd for DirtyFlags {
8195 type Output = DirtyFlags;
8196 fn bitand(self, rhs: DirtyFlags) -> DirtyFlags {
8197 DirtyFlags(self.0 & rhs.0)
8198 }
8199}
8200
8201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8207pub struct InvalidationRecord {
8208 pub id: KvasirId,
8210 pub flags: DirtyFlags,
8212}
8213
8214impl InvalidationRecord {
8215 pub fn new(id: KvasirId, flags: DirtyFlags) -> Self {
8217 Self { id, flags }
8218 }
8219
8220 pub fn full(id: KvasirId) -> Self {
8222 Self { id, flags: DirtyFlags::ALL }
8223 }
8224}
8225
8226#[cfg(test)]
8227mod kvasir_identity_tests {
8228 use super::*;
8229
8230 #[test]
8231 fn kvasir_id_new_is_non_zero() {
8232 let id = KvasirId::new();
8234 assert!(!id.is_null(), "KvasirId::new() returned null sentinel");
8235 }
8236
8237 #[test]
8238 fn kvasir_id_new_is_unique() {
8239 let a = KvasirId::new();
8241 let b = KvasirId::new();
8242 let c = KvasirId::new();
8243 assert_ne!(a, b);
8244 assert_ne!(b, c);
8245 assert_ne!(a, c);
8246 }
8247
8248 #[test]
8249 fn kvasir_id_null_sentinel() {
8250 assert!(KvasirId::NULL.is_null());
8251 assert!(!KvasirId::new().is_null());
8252 }
8253
8254 #[test]
8255 fn kvasir_id_serde_roundtrip() {
8256 let id = KvasirId(42);
8257 let json = serde_json::to_string(&id).unwrap();
8258 let decoded: KvasirId = serde_json::from_str(&json).unwrap();
8259 assert_eq!(id, decoded);
8260 }
8261
8262 #[test]
8263 fn dirty_flags_clean_is_not_dirty() {
8264 assert!(!DirtyFlags::CLEAN.is_dirty());
8265 }
8266
8267 #[test]
8268 fn dirty_flags_all_implies_all_layers() {
8269 let f = DirtyFlags::ALL;
8270 assert!(f.needs_state());
8271 assert!(f.needs_layout());
8272 assert!(f.needs_paint());
8273 assert!(f.needs_composite());
8274 }
8275
8276 #[test]
8277 fn dirty_flags_composite_only() {
8278 let f = DirtyFlags::COMPOSITE;
8279 assert!(!f.needs_state());
8280 assert!(!f.needs_layout());
8281 assert!(!f.needs_paint());
8282 assert!(f.needs_composite());
8283 }
8284
8285 #[test]
8286 fn dirty_flags_merge() {
8287 let a = DirtyFlags::COMPOSITE;
8288 let b = DirtyFlags::PAINT;
8289 let merged = a.merge(b);
8290 assert!(merged.needs_composite());
8291 assert!(merged.needs_paint());
8292 assert!(!merged.needs_layout());
8293 }
8294
8295 #[test]
8296 fn dirty_flags_bitor() {
8297 let combined = DirtyFlags::PAINT | DirtyFlags::COMPOSITE;
8298 assert!(combined.needs_paint());
8299 assert!(combined.needs_composite());
8300 }
8301
8302 #[test]
8303 fn dirty_flags_clear() {
8304 let dirty = DirtyFlags::ALL;
8305 let clean = dirty.clear();
8306 assert!(!clean.is_dirty());
8307 }
8308
8309 #[test]
8310 fn dirty_flags_serde_roundtrip() {
8311 let f = DirtyFlags::LAYOUT;
8312 let json = serde_json::to_string(&f).unwrap();
8313 let decoded: DirtyFlags = serde_json::from_str(&json).unwrap();
8314 assert_eq!(f, decoded);
8315 }
8316
8317 #[test]
8318 fn invalidation_record_full() {
8319 let id = KvasirId::new();
8320 let rec = InvalidationRecord::full(id);
8321 assert_eq!(rec.id, id);
8322 assert!(rec.flags.needs_state());
8323 assert!(rec.flags.needs_layout());
8324 }
8325}
8326
8327#[cfg(test)]
8337mod subscriber_panic_isolation_tests {
8338 use super::*;
8339 use std::sync::atomic::{AtomicUsize, Ordering};
8340
8341 #[test]
8342 fn panicking_subscriber_does_not_poison_mutex() {
8343 let state = State::new(0i32);
8344 let fired = Arc::new(AtomicUsize::new(0));
8345
8346 let _ = state.subscribe(|_| -> () {
8348 panic!("subscriber 1 explodes");
8349 });
8350
8351 let fired_clone = Arc::clone(&fired);
8353 let _ = state.subscribe(move |v| {
8354 fired_clone.store(*v as usize + 1, Ordering::SeqCst);
8355 });
8356
8357 state.set(42);
8359
8360 assert_eq!(
8361 fired.load(Ordering::SeqCst),
8362 43,
8363 "second subscriber must fire even though first panicked"
8364 );
8365
8366 let fired2 = Arc::new(AtomicUsize::new(0));
8368 let fired2_clone = Arc::clone(&fired2);
8369 let _ = state.subscribe(move |v| {
8370 fired2_clone.store(*v as usize, Ordering::SeqCst);
8371 });
8372 state.set(100);
8373 assert_eq!(
8374 fired2.load(Ordering::SeqCst),
8375 100,
8376 "future updates must work after subscriber panic"
8377 );
8378 }
8379
8380 #[test]
8381 fn all_subscribers_fire_even_if_one_panics() {
8382 let state = State::new(0u32);
8383 let count = Arc::new(AtomicUsize::new(0));
8384
8385 let _ = state.subscribe(|_| panic!("boom 1"));
8387 let c1 = Arc::clone(&count);
8388 let _ = state.subscribe(move |_| {
8389 c1.fetch_add(1, Ordering::SeqCst);
8390 });
8391 let _ = state.subscribe(|_| panic!("boom 2"));
8392 let c2 = Arc::clone(&count);
8393 let _ = state.subscribe(move |_| {
8394 c2.fetch_add(1, Ordering::SeqCst);
8395 });
8396
8397 state.set(1);
8398
8399 assert_eq!(
8401 count.load(Ordering::SeqCst),
8402 2,
8403 "both non-panicking subscribers should fire"
8404 );
8405 }
8406
8407 #[test]
8408 fn invoke_subscribers_safely_returns_count() {
8409 use std::sync::Mutex;
8411 let subs: SubscriberList<u32> = Arc::new(Mutex::new(Vec::new()));
8412
8413 let count1 = Arc::new(AtomicUsize::new(0));
8414 let count1_clone = Arc::clone(&count1);
8415 subs.lock().unwrap().push(Box::new(move |v| {
8416 count1_clone.store(*v as usize, Ordering::SeqCst);
8417 }));
8418
8419 let count2 = Arc::new(AtomicUsize::new(0));
8420 let count2_clone = Arc::clone(&count2);
8421 subs.lock().unwrap().push(Box::new(move |v| {
8422 count2_clone.store(*v as usize + 100, Ordering::SeqCst);
8423 }));
8424
8425 let invoked = invoke_subscribers_safely(&subs, &7);
8426 assert_eq!(invoked, 2, "both subscribers should be invoked");
8427 assert_eq!(count1.load(Ordering::SeqCst), 7);
8428 assert_eq!(count2.load(Ordering::SeqCst), 107);
8429 }
8430}
8431
8432#[cfg(test)]
8441mod p1_17_shared_fallback_runtime_tests {
8442 use super::*;
8443 use std::time::Duration;
8444
8445 #[test]
8446 fn fallback_runtime_is_shared() {
8447 let r1 = fallback_runtime();
8451 let r2 = fallback_runtime();
8452 assert!(
8453 std::ptr::eq(r1 as *const _, r2 as *const _),
8454 "fallback_runtime must return the same instance"
8455 );
8456 }
8457
8458 #[test]
8459 fn fallback_worker_count_is_bounded() {
8460 let n = *FALLBACK_WORKER_COUNT.get_or_init(|| {
8464 let available = std::thread::available_parallelism()
8465 .map(|n| n.get())
8466 .unwrap_or(2);
8467 available.saturating_sub(1).clamp(1, 8)
8468 });
8469 assert!(n >= 1, "worker count must be at least 1, got {n}");
8470 assert!(n <= 8, "worker count must be at most 8, got {n}");
8471 }
8472
8473 #[test]
8474 fn many_suspense_calls_share_runtime() {
8475 let counter = State::new(0u32);
8483 let mut handles = Vec::new();
8484 for _ in 0..20 {
8485 let s = Suspense::new_async(async { Ok::<u32, String>(1) });
8486 let _ = s; handles.push(s);
8492 }
8493 counter.set(20);
8495 assert_eq!(counter.get(), 20);
8496 }
8499
8500 #[test]
8505 fn p1_14_state_storage_mechanisms() {
8506 use std::mem::size_of;
8513 let state = State::new(42u32);
8514 let size = size_of_val(&state);
8518 assert!(
8522 size >= 4 * std::mem::size_of::<usize>(),
8523 "State<T> should be at least 4 Arcs in size"
8524 );
8525 }
8526
8527 #[test]
8528 fn p1_14_set_direct_updates_value() {
8529 let state = State::new(0u32);
8532 state.set_direct(42);
8533 assert_eq!(state.get(), 42);
8534 }
8535
8536 #[test]
8537 fn p1_14_set_direct_notifies_subscribers() {
8538 let state = State::new(0u32);
8541 let received = Arc::new(Mutex::new(Vec::new()));
8542 let received_clone = Arc::clone(&received);
8543 state.subscribe(move |v| {
8544 received_clone.lock().unwrap().push(*v);
8545 });
8546 state.set_direct(1);
8547 state.set_direct(2);
8548 state.set_direct(3);
8549 std::thread::sleep(std::time::Duration::from_millis(10));
8551 let log = received.lock().unwrap();
8552 assert!(
8556 log.contains(&1) && log.contains(&2) && log.contains(&3),
8557 "set_direct must notify subscribers of all values"
8558 );
8559 }
8560}
8561
8562#[derive(Debug, Clone, Default)]
8585pub struct DirtyRegionManager {
8586 regions: Vec<Rect>,
8588 generation: u64,
8591}
8592
8593impl DirtyRegionManager {
8594 pub fn new() -> Self {
8596 Self::default()
8597 }
8598
8599 pub fn mark_dirty(&mut self, region: Rect) {
8608 for existing in self.regions.iter_mut() {
8610 if Self::rects_overlap(*existing, region) {
8611 *existing = Self::union_rect(*existing, region);
8612 return;
8613 }
8614 }
8615 self.regions.push(region);
8617 }
8618
8619 pub fn regions(&self) -> &[Rect] {
8622 &self.regions
8623 }
8624
8625 pub fn is_dirty(&self) -> bool {
8628 !self.regions.is_empty()
8629 }
8630
8631 pub fn clear(&mut self) {
8637 self.regions.clear();
8638 self.generation = self.generation.wrapping_add(1);
8639 }
8640
8641 pub fn generation(&self) -> u64 {
8645 self.generation
8646 }
8647
8648 pub fn len(&self) -> usize {
8652 self.regions.len()
8653 }
8654
8655 pub fn is_empty(&self) -> bool {
8657 self.regions.is_empty()
8658 }
8659
8660 fn rects_overlap(a: Rect, b: Rect) -> bool {
8662 a.x < b.x + b.width
8663 && a.x + a.width > b.x
8664 && a.y < b.y + b.height
8665 && a.y + a.height > b.y
8666 }
8667
8668 fn union_rect(a: Rect, b: Rect) -> Rect {
8671 let min_x = a.x.min(b.x);
8672 let min_y = a.y.min(b.y);
8673 let max_x = (a.x + a.width).max(b.x + b.width);
8674 let max_y = (a.y + a.height).max(b.y + b.height);
8675 Rect {
8676 x: min_x,
8677 y: min_y,
8678 width: max_x - min_x,
8679 height: max_y - min_y,
8680 }
8681 }
8682}
8683
8684#[cfg(test)]
8685mod p1_39_dirty_region_tests {
8686 use super::{DirtyRegionManager, Rect};
8687
8688 #[test]
8689 fn new_manager_is_empty() {
8690 let m = DirtyRegionManager::new();
8691 assert!(!m.is_dirty());
8692 assert!(m.is_empty());
8693 assert_eq!(m.len(), 0);
8694 }
8695
8696 #[test]
8697 fn mark_dirty_adds_region() {
8698 let mut m = DirtyRegionManager::new();
8699 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8700 assert!(m.is_dirty());
8701 assert_eq!(m.len(), 1);
8702 }
8703
8704 #[test]
8705 fn overlapping_regions_coalesce() {
8706 let mut m = DirtyRegionManager::new();
8707 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8708 m.mark_dirty(Rect { x: 5.0, y: 5.0, width: 10.0, height: 10.0 });
8709 assert_eq!(m.len(), 1);
8711 let r = &m.regions()[0];
8712 assert_eq!(r.x, 0.0);
8713 assert_eq!(r.y, 0.0);
8714 assert_eq!(r.width, 15.0);
8715 assert_eq!(r.height, 15.0);
8716 }
8717
8718 #[test]
8719 fn non_overlapping_regions_dont_coalesce() {
8720 let mut m = DirtyRegionManager::new();
8721 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8722 m.mark_dirty(Rect { x: 100.0, y: 100.0, width: 10.0, height: 10.0 });
8723 assert_eq!(m.len(), 2);
8725 }
8726
8727 #[test]
8728 fn clear_resets_regions_and_increments_generation() {
8729 let mut m = DirtyRegionManager::new();
8730 m.mark_dirty(Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
8731 let g1 = m.generation();
8732 m.clear();
8733 assert!(!m.is_dirty());
8734 assert_eq!(m.len(), 0);
8735 assert_eq!(m.generation(), g1 + 1);
8736 }
8737
8738 #[test]
8739 fn many_overlapping_marks_coalesce_to_one() {
8740 let mut m = DirtyRegionManager::new();
8741 for i in 0..100 {
8743 m.mark_dirty(Rect {
8744 x: i as f32,
8745 y: i as f32,
8746 width: 10.0,
8747 height: 10.0,
8748 });
8749 }
8750 assert_eq!(m.len(), 1);
8752 }
8753}
8754
8755#[derive(Debug, Clone, PartialEq)]
8786pub struct VirtualWindow {
8787 pub first_visible: usize,
8789 pub last_visible: usize,
8791 pub offset_before: f32,
8794 pub offset_after: f32,
8797}
8798
8799pub fn compute_virtual_list_window(
8812 total_rows: usize,
8813 row_height: f32,
8814 viewport_y: f32,
8815 viewport_height: f32,
8816 overscan: usize,
8817) -> VirtualWindow {
8818 if total_rows == 0 || row_height <= 0.0 {
8819 return VirtualWindow {
8820 first_visible: 0,
8821 last_visible: 0,
8822 offset_before: 0.0,
8823 offset_after: 0.0,
8824 };
8825 }
8826
8827 let visible_rows = (viewport_height / row_height).ceil() as usize;
8829
8830 let first = (viewport_y / row_height).floor() as isize - overscan as isize;
8832 let first = first.max(0) as usize;
8833
8834 let last = first + visible_rows + 2 * overscan;
8836 let last = last.min(total_rows);
8837
8838 VirtualWindow {
8839 first_visible: first,
8840 last_visible: last,
8841 offset_before: first as f32 * row_height,
8842 offset_after: (total_rows - last) as f32 * row_height,
8843 }
8844}
8845
8846pub fn compute_virtual_list_window_variable(
8859 prefix_heights: &[f32],
8860 viewport_y: f32,
8861 viewport_height: f32,
8862 overscan: usize,
8863) -> VirtualWindow {
8864 let total_rows = prefix_heights.len().saturating_sub(1);
8865 if total_rows == 0 {
8866 return VirtualWindow {
8867 first_visible: 0,
8868 last_visible: 0,
8869 offset_before: 0.0,
8870 offset_after: 0.0,
8871 };
8872 }
8873
8874 let first_idx = prefix_heights
8876 .partition_point(|&h| h < viewport_y)
8877 .saturating_sub(1);
8878 let first = first_idx.saturating_sub(overscan);
8879
8880 let viewport_bottom = viewport_y + viewport_height;
8882 let last_idx = prefix_heights.partition_point(|&h| h < viewport_bottom);
8883 let last = (last_idx + overscan).min(total_rows);
8884
8885 VirtualWindow {
8886 first_visible: first,
8887 last_visible: last,
8888 offset_before: prefix_heights[first],
8889 offset_after: prefix_heights[total_rows] - prefix_heights[last],
8890 }
8891}
8892
8893pub struct DependencyGraph {
8908 deps: std::collections::HashMap<u64, std::collections::HashSet<u64>>,
8911 reverse: std::collections::HashMap<u64, Vec<u64>>,
8914}
8915
8916impl DependencyGraph {
8917 pub fn new() -> Self {
8919 Self {
8920 deps: std::collections::HashMap::new(),
8921 reverse: std::collections::HashMap::new(),
8922 }
8923 }
8924
8925 pub fn register(&mut self, component_id: u64, state_key: u64) {
8930 self.deps
8931 .entry(state_key)
8932 .or_default()
8933 .insert(component_id);
8934 self.reverse
8935 .entry(component_id)
8936 .or_default()
8937 .push(state_key);
8938 }
8939
8940 pub fn unregister(&mut self, component_id: u64) {
8943 if let Some(keys) = self.reverse.remove(&component_id) {
8944 for key in keys {
8945 if let Some(set) = self.deps.get_mut(&key) {
8946 set.remove(&component_id);
8947 }
8948 }
8949 }
8950 }
8951
8952 pub fn affected_components(&self, state_key: u64) -> impl Iterator<Item = u64> + '_ {
8958 self.deps
8959 .get(&state_key)
8960 .into_iter()
8961 .flat_map(|set| set.iter().copied())
8962 }
8963
8964 pub fn has_dependents(&self, state_key: u64) -> bool {
8966 self.deps
8967 .get(&state_key)
8968 .map_or(false, |set| !set.is_empty())
8969 }
8970
8971 pub fn edge_count(&self) -> usize {
8973 self.deps.values().map(|s| s.len()).sum()
8974 }
8975}
8976
8977impl Default for DependencyGraph {
8978 fn default() -> Self {
8979 Self::new()
8980 }
8981}
8982
8983use std::time::{Duration, Instant};
8984
8985#[derive(Debug, Clone, Copy, PartialEq)]
8989pub struct SubsystemBudget {
8990 pub time_slice: Duration,
8992 pub skippable: bool,
8996 pub name: &'static str,
8998}
8999
9000#[derive(Debug)]
9008pub struct FrameBudgetTracker {
9009 total: Duration,
9012 allocations: Vec<SubsystemBudget>,
9015 start: Option<Instant>,
9017 elapsed: Vec<Duration>,
9019}
9020
9021impl FrameBudgetTracker {
9022 pub fn default_60fps() -> Self {
9027 Self {
9028 total: Duration::from_micros(16_666), allocations: vec![
9030 SubsystemBudget {
9031 time_slice: Duration::from_micros(4_000),
9032 skippable: true,
9033 name: "animation",
9034 },
9035 SubsystemBudget {
9036 time_slice: Duration::from_micros(4_000),
9037 skippable: true,
9038 name: "layout",
9039 },
9040 SubsystemBudget {
9041 time_slice: Duration::from_micros(8_000),
9042 skippable: false, name: "render",
9044 },
9045 ],
9046 start: None,
9047 elapsed: vec![
9048 Duration::ZERO,
9049 Duration::ZERO,
9050 Duration::ZERO,
9051 ],
9052 }
9053 }
9054
9055 pub fn default_120fps() -> Self {
9059 Self {
9060 total: Duration::from_micros(8_333), allocations: vec![
9062 SubsystemBudget {
9063 time_slice: Duration::from_micros(2_000),
9064 skippable: true,
9065 name: "animation",
9066 },
9067 SubsystemBudget {
9068 time_slice: Duration::from_micros(2_000),
9069 skippable: true,
9070 name: "layout",
9071 },
9072 SubsystemBudget {
9073 time_slice: Duration::from_micros(4_000),
9074 skippable: false, name: "render",
9076 },
9077 ],
9078 start: None,
9079 elapsed: vec![
9080 Duration::ZERO,
9081 Duration::ZERO,
9082 Duration::ZERO,
9083 ],
9084 }
9085 }
9086
9087 pub fn total(&self) -> Duration {
9089 self.total
9090 }
9091
9092 pub fn allocations(&self) -> &[SubsystemBudget] {
9094 &self.allocations
9095 }
9096
9097 pub fn new_frame(&mut self) {
9100 self.start = Some(Instant::now());
9101 for e in self.elapsed.iter_mut() {
9102 *e = Duration::ZERO;
9103 }
9104 }
9105
9106 pub fn subsystem_finish(&mut self, index: usize) {
9109 if let Some(start) = self.start {
9110 if index < self.elapsed.len() {
9111 let now = Instant::now();
9112 self.elapsed[index] = now.duration_since(start);
9113 }
9114 }
9115 }
9116
9117 pub fn is_within_budget(&self, index: usize) -> bool {
9121 if index >= self.allocations.len() {
9122 return false;
9123 }
9124 if index >= self.elapsed.len() {
9125 return false;
9126 }
9127 self.elapsed[index] <= self.allocations[index].time_slice
9128 }
9129
9130 pub fn frame_within_budget(&self) -> bool {
9134 for (i, alloc) in self.allocations.iter().enumerate() {
9135 if i < self.elapsed.len()
9136 && self.elapsed[i] > alloc.time_slice
9137 && !alloc.skippable
9138 {
9139 return false;
9140 }
9141 }
9142 true
9143 }
9144
9145 pub fn elapsed(&self, index: usize) -> Duration {
9147 if index < self.elapsed.len() {
9148 self.elapsed[index]
9149 } else {
9150 Duration::ZERO
9151 }
9152 }
9153
9154 pub fn total_elapsed(&self) -> Duration {
9156 match self.start {
9157 Some(start) => start.elapsed(),
9158 None => Duration::ZERO,
9159 }
9160 }
9161}
9162
9163#[cfg(test)]
9164mod p1_43_frame_budget_tests {
9165 use super::FrameBudgetTracker;
9166
9167 #[test]
9168 fn default_60fps_has_16ms_total() {
9169 let fb = FrameBudgetTracker::default_60fps();
9170 assert!(fb.total().as_micros() >= 16_000);
9172 assert!(fb.total().as_micros() <= 17_000);
9173 }
9174
9175 #[test]
9176 fn default_allocations_sum_to_roughly_total() {
9177 let fb = FrameBudgetTracker::default_60fps();
9178 let sum: u128 = fb.allocations().iter()
9179 .map(|a| a.time_slice.as_micros())
9180 .sum();
9181 let total = fb.total().as_micros();
9184 assert!(sum <= total);
9185 assert!(sum >= total - 1_000); }
9187
9188 #[test]
9189 fn render_is_essential_layout_is_skippable() {
9190 let fb = FrameBudgetTracker::default_60fps();
9191 let render = fb.allocations().iter().find(|a| a.name == "render").unwrap();
9192 let layout = fb.allocations().iter().find(|a| a.name == "layout").unwrap();
9193 assert!(!render.skippable, "render must always run");
9194 assert!(layout.skippable, "layout can be skipped if over budget");
9195 }
9196
9197 #[test]
9198 fn new_frame_resets_state() {
9199 let mut fb = FrameBudgetTracker::default_60fps();
9200 fb.new_frame();
9201 for i in 0..fb.allocations().len() {
9203 assert_eq!(fb.elapsed(i).as_nanos(), 0);
9204 }
9205 }
9206
9207 #[test]
9208 fn is_within_budget_initially_true() {
9209 let mut fb = FrameBudgetTracker::default_60fps();
9210 fb.new_frame();
9211 for i in 0..fb.allocations().len() {
9214 assert!(fb.is_within_budget(i));
9215 }
9216 }
9217
9218 #[test]
9219 fn frame_within_budget_initially_true() {
9220 let mut fb = FrameBudgetTracker::default_60fps();
9221 fb.new_frame();
9222 assert!(fb.frame_within_budget());
9223 }
9224}
9225
9226#[derive(Debug, Clone)]
9232pub struct InputLatencyTracker {
9233 window_size: usize,
9235 samples: std::collections::VecDeque<(Instant, Instant)>,
9237}
9238
9239impl InputLatencyTracker {
9240 pub fn new(window_size: usize) -> Self {
9245 Self {
9246 window_size,
9247 samples: std::collections::VecDeque::with_capacity(window_size),
9248 }
9249 }
9250
9251 pub fn record_frame(&mut self, event_time: Instant, render_time: Instant) {
9261 if self.window_size == 0 {
9262 return;
9263 }
9264 if self.samples.len() >= self.window_size {
9265 self.samples.pop_front();
9266 }
9267 self.samples.push_back((event_time, render_time));
9268 }
9269
9270 pub fn percentile(&self, p: f64) -> Duration {
9283 if self.samples.is_empty() || p < 0.0 || p > 100.0 {
9284 return Duration::ZERO;
9285 }
9286 let mut latencies: Vec<Duration> = self.samples
9287 .iter()
9288 .map(|&(e, r)| {
9289 if r > e {
9290 r.duration_since(e)
9291 } else {
9292 Duration::ZERO
9293 }
9294 })
9295 .collect();
9296 latencies.sort();
9297 let len = latencies.len();
9298 let rank = p / 100.0;
9299 let index = ((len as f64 * rank).ceil() as usize).saturating_sub(1);
9300 let index = index.min(len - 1);
9301 latencies[index]
9302 }
9303
9304 pub fn clear(&mut self) {
9309 self.samples.clear();
9310 }
9311
9312 pub fn window_size(&self) -> usize {
9314 self.window_size
9315 }
9316
9317 pub fn set_window_size(&mut self, size: usize) {
9326 self.window_size = size;
9327 while self.samples.len() > self.window_size {
9328 self.samples.pop_front();
9329 }
9330 }
9331
9332 pub fn len(&self) -> usize {
9334 self.samples.len()
9335 }
9336
9337 pub fn is_empty(&self) -> bool {
9339 self.samples.is_empty()
9340 }
9341}
9342
9343#[cfg(test)]
9344mod p2_36_input_latency_tests {
9345 use super::InputLatencyTracker;
9346 use std::time::{Duration, Instant};
9347
9348 #[test]
9349 fn test_empty_tracker() {
9350 let tracker = InputLatencyTracker::new(10);
9351 assert!(tracker.is_empty());
9352 assert_eq!(tracker.len(), 0);
9353 assert_eq!(tracker.percentile(50.0), Duration::ZERO);
9354 }
9355
9356 #[test]
9357 fn test_record_and_sliding_window() {
9358 let mut tracker = InputLatencyTracker::new(3);
9359 let now = Instant::now();
9360 tracker.record_frame(now, now + Duration::from_millis(10));
9361 tracker.record_frame(now, now + Duration::from_millis(20));
9362 tracker.record_frame(now, now + Duration::from_millis(30));
9363 assert_eq!(tracker.len(), 3);
9364
9365 tracker.record_frame(now, now + Duration::from_millis(40));
9367 assert_eq!(tracker.len(), 3);
9368
9369 assert_eq!(tracker.percentile(50.0), Duration::from_millis(30));
9371 assert_eq!(tracker.percentile(0.0), Duration::from_millis(20));
9372 assert_eq!(tracker.percentile(100.0), Duration::from_millis(40));
9373 }
9374
9375 #[test]
9376 fn test_resize_and_clear() {
9377 let mut tracker = InputLatencyTracker::new(5);
9378 let now = Instant::now();
9379 for i in 1..=5 {
9380 tracker.record_frame(now, now + Duration::from_millis(i * 10));
9381 }
9382 assert_eq!(tracker.len(), 5);
9383
9384 tracker.set_window_size(3);
9385 assert_eq!(tracker.window_size(), 3);
9386 assert_eq!(tracker.len(), 3);
9387 assert_eq!(tracker.percentile(0.0), Duration::from_millis(30));
9389 assert_eq!(tracker.percentile(100.0), Duration::from_millis(50));
9390
9391 tracker.clear();
9392 assert!(tracker.is_empty());
9393 assert_eq!(tracker.percentile(50.0), Duration::ZERO);
9394 }
9395
9396 #[test]
9397 fn test_invalid_percentiles() {
9398 let mut tracker = InputLatencyTracker::new(2);
9399 let now = Instant::now();
9400 tracker.record_frame(now, now + Duration::from_millis(10));
9401 assert_eq!(tracker.percentile(-1.0), Duration::ZERO);
9402 assert_eq!(tracker.percentile(101.0), Duration::ZERO);
9403 }
9404}
9405
9406#[cfg(test)]
9411mod p1_40_event_phase_tests {
9412 use super::EventPhase;
9413
9414 #[test]
9415 fn all_phases_in_capture_order() {
9416 let phases = EventPhase::ALL;
9418 assert_eq!(phases[0], EventPhase::Capture);
9419 assert_eq!(phases[1], EventPhase::Target);
9420 assert_eq!(phases[2], EventPhase::Bubble);
9421 assert_eq!(phases.len(), 3);
9422 }
9423
9424 #[test]
9425 fn phase_distinct() {
9426 let capture = EventPhase::Capture;
9428 let target = EventPhase::Target;
9429 let bubble = EventPhase::Bubble;
9430 assert_ne!(capture, target);
9431 assert_ne!(target, bubble);
9432 assert_ne!(capture, bubble);
9433 }
9434
9435 #[test]
9436 fn phase_clone_preserves_value() {
9437 let phase = EventPhase::Bubble;
9439 let copied = phase;
9440 assert_eq!(phase, copied);
9441 }
9442}
9443
9444#[cfg(test)]
9448mod p1_41_virtual_list_tests {
9449 use super::{compute_virtual_list_window, compute_virtual_list_window_variable};
9450
9451 #[test]
9452 fn uniform_list_at_top_shows_first_rows() {
9453 let w = compute_virtual_list_window(1000, 20.0, 0.0, 200.0, 2);
9455 assert_eq!(w.first_visible, 0, "first_visible should be clamped to 0");
9457 assert!(w.last_visible <= 14, "last_visible should not exceed 14 at top");
9460 assert_eq!(w.offset_before, 0.0);
9461 assert!(w.offset_after > 0.0, "tail placeholder must be > 0");
9462 }
9463
9464 #[test]
9465 fn uniform_list_mid_scroll_correct_window() {
9466 let w = compute_virtual_list_window(1000, 20.0, 1000.0, 200.0, 2);
9468 assert!(w.first_visible <= 48, "must include overscan rows above 50");
9469 assert!(w.last_visible > 60, "must include rows below visible");
9470 let expected_before = w.first_visible as f32 * 20.0;
9472 assert!((w.offset_before - expected_before).abs() < 1.0);
9473 }
9474
9475 #[test]
9476 fn uniform_list_near_end_does_not_exceed_total() {
9477 let w = compute_virtual_list_window(10, 20.0, 150.0, 100.0, 5);
9478 assert!(w.last_visible <= 10, "must not exceed total rows");
9479 assert!(w.offset_after >= 0.0);
9480 }
9481
9482 #[test]
9483 fn variable_list_basic() {
9484 let prefix: Vec<f32> = vec![0.0, 10.0, 30.0, 60.0, 100.0, 150.0];
9486 let w = compute_virtual_list_window_variable(&prefix, 0.0, 40.0, 0);
9488 assert!(w.first_visible == 0);
9489 assert!(w.last_visible >= 3 && w.last_visible <= 5);
9490 }
9491
9492 #[test]
9493 fn empty_list_returns_zero_window() {
9494 let w = compute_virtual_list_window(0, 20.0, 0.0, 200.0, 2);
9495 assert_eq!(w.first_visible, 0);
9496 assert_eq!(w.last_visible, 0);
9497 assert_eq!(w.offset_before, 0.0);
9498 assert_eq!(w.offset_after, 0.0);
9499 }
9500}
9501
9502#[cfg(test)]
9506mod p1_42_dependency_graph_tests {
9507 use super::DependencyGraph;
9508
9509 #[test]
9510 fn register_and_query_single_dep() {
9511 let mut g = DependencyGraph::new();
9512 g.register(42, 100); let affected: Vec<u64> = g.affected_components(100).collect();
9514 assert_eq!(affected, vec![42]);
9515 }
9516
9517 #[test]
9518 fn unregister_removes_component() {
9519 let mut g = DependencyGraph::new();
9520 g.register(1, 10);
9521 g.register(2, 10);
9522 g.unregister(1);
9523 let affected: Vec<u64> = g.affected_components(10).collect();
9524 assert!(!affected.contains(&1), "component 1 must be gone after unregister");
9525 assert!(affected.contains(&2), "component 2 must still be present");
9526 }
9527
9528 #[test]
9529 fn no_deps_returns_empty() {
9530 let g = DependencyGraph::new();
9531 let affected: Vec<u64> = g.affected_components(999).collect();
9532 assert!(affected.is_empty());
9533 assert!(!g.has_dependents(999));
9534 }
9535
9536 #[test]
9537 fn edge_count_reflects_registrations() {
9538 let mut g = DependencyGraph::new();
9539 assert_eq!(g.edge_count(), 0);
9540 g.register(1, 10);
9541 g.register(2, 10);
9542 g.register(1, 20); assert_eq!(g.edge_count(), 3);
9544 }
9545
9546 #[test]
9547 fn re_register_after_unregister_works() {
9548 let mut g = DependencyGraph::new();
9549 g.register(5, 50);
9550 g.unregister(5);
9551 g.register(5, 60);
9553 assert!(!g.has_dependents(50), "old key must be gone");
9554 assert!(g.has_dependents(60), "new key must be present");
9555 }
9556}