Seregon/StratoSDK

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

Rust/27.3 KB/No license
crates/strato-core/src/taffy_layout.rs
StratoSDK / crates / strato-core / src / taffy_layout.rs
1//! Taffy Layout Engine Integration for StratoUI
2//!
3//! This module provides integration with the Taffy layout engine for Flexbox/Grid
4//! automatic layout computation.
5//!
6//! # Architecture
7//!
8//! ```text
9//! Widget Tree (user code)
10//! ↓
11//! TaffyLayoutManager::compute()
12//! ↓
13//! Taffy Tree (internal)
14//! ↓
15//! taffy.compute_layout()
16//! ↓
17//! ComputedLayout (validated coordinates)
18//! ↓
19//! Render Loop (wgpu)
20//! ```
21//!
22//! # Safety Guarantees
23//!
24//! 1. **Zero-Panic**: All errors handled with Result
25//! 2. **Validated Coordinates**: ValidatedRect guarantees finite, non-negative values
26//! 3. **Graceful Degradation**: Fallback to last valid layout if compute fails
27//!
28//! # Performance
29//!
30//! - Target: < 5ms per 1000 nodes
31//! - Frame budget: 16ms (60 FPS)
32 
33pub use crate::error::{TaffyLayoutError, TaffyLayoutResult};
34use crate::error::{TaffyValidationError, TaffyValidationResult};
35use crate::layout::EdgeInsets;
36use crate::validated_rect::ValidatedRect;
37 
38use taffy::prelude::*;
39 
40#[cfg(feature = "perf-metrics")]
41use std::time::{Duration, Instant};
42 
43// =============================================================================
44// Core Types
45// =============================================================================
46 
47/// Result of a layout computation - list of draw commands in Painter's Algorithm order.
48///
49/// # Thread Safety
50///
51/// NOT thread-safe. Must be used from the main thread only.
52#[derive(Debug, Clone)]
53pub struct ComputedLayout {
54 /// Draw commands in rendering order (Painter's Algorithm).
55 draw_commands: Vec<DrawCommand>,
56}
57 
58impl ComputedLayout {
59 /// Create a new computed layout with the given draw commands.
60 fn new(draw_commands: Vec<DrawCommand>) -> Self {
61 Self { draw_commands }
62 }
63 
64 /// Get the draw commands for rendering.
65 #[inline]
66 pub fn draw_commands(&self) -> &[DrawCommand] {
67 &self.draw_commands
68 }
69 
70 /// Get the number of draw commands.
71 #[inline]
72 pub fn len(&self) -> usize {
73 self.draw_commands.len()
74 }
75 
76 /// Check if the layout is empty.
77 #[inline]
78 pub fn is_empty(&self) -> bool {
79 self.draw_commands.is_empty()
80 }
81}
82 
83impl Default for ComputedLayout {
84 fn default() -> Self {
85 Self {
86 draw_commands: Vec::new(),
87 }
88 }
89}
90 
91/// A single draw command with validated coordinates.
92///
93/// # Invariants
94///
95/// - `viewport` coordinates are GUARANTEED valid by ValidatedRect
96/// - Draw commands are ordered for Painter's Algorithm (back-to-front)
97#[derive(Debug, Clone)]
98pub struct DrawCommand {
99 /// Taffy node ID for widget lookup.
100 pub node: NodeId,
101 
102 /// Validated viewport with GPU-safe coordinates.
103 pub viewport: ValidatedRect,
104 
105 /// Depth in the tree (0 = root).
106 pub depth: usize,
107}
108 
109impl DrawCommand {
110 /// Create a new draw command.
111 ///
112 /// # Arguments
113 ///
114 /// * `node` - Taffy NodeId
115 /// * `viewport` - Validated rectangle
116 /// * `depth` - Tree depth (0 = root)
117 pub fn new(node: NodeId, viewport: ValidatedRect, depth: usize) -> Self {
118 Self {
119 node,
120 viewport,
121 depth,
122 }
123 }
124}
125 
126// =============================================================================
127// TaffyWidget Trait
128// =============================================================================
129 
130/// Trait for widgets that can participate in Taffy layout.
131///
132/// This is a parallel trait to the existing Widget trait, specifically for
133/// Taffy-compatible widgets.
134///
135/// # Example
136///
137/// ```rust,ignore
138/// struct MyButton {
139/// label: String,
140/// size: (f32, f32),
141/// }
142///
143/// impl TaffyWidget for MyButton {
144/// fn build_layout(&self, tree: &mut TaffyTree<()>) -> TaffyLayoutResult<NodeId> {
145/// let style = Style {
146/// size: Size {
147/// width: length(self.size.0),
148/// height: length(self.size.1),
149/// },
150/// ..Default::default()
151/// };
152/// tree.new_leaf(style).map_err(Into::into)
153/// }
154/// }
155/// ```
156pub trait TaffyWidget: std::fmt::Debug {
157 /// Build the Taffy layout node for this widget.
158 ///
159 /// # Arguments
160 ///
161 /// * `tree` - Mutable reference to the Taffy tree
162 ///
163 /// # Returns
164 ///
165 /// * `Ok(NodeId)` - The created node ID
166 /// * `Err(TaffyLayoutError)` - If node creation fails
167 ///
168 /// # Errors
169 ///
170 /// Returns `TaffyLayoutError::NodeBuildFailed` if Taffy fails to create the node.
171 fn build_layout(&self, tree: &mut TaffyTree<()>) -> TaffyLayoutResult<NodeId>;
172 
173 /// Validate this widget's configuration before layout.
174 ///
175 /// # Returns
176 ///
177 /// * `Ok(())` - If validation passes
178 /// * `Err(TaffyValidationError)` - If validation fails
179 ///
180 /// # Default
181 ///
182 /// Default implementation returns `Ok(())`.
183 fn validate(&self) -> TaffyValidationResult<()> {
184 Ok(())
185 }
186 
187 /// Get child widgets for container widgets.
188 ///
189 /// # Returns
190 ///
191 /// Slice of child widgets (empty for leaf widgets).
192 ///
193 /// # Default
194 ///
195 /// Default implementation returns an empty slice.
196 fn taffy_children(&self) -> &[Box<dyn TaffyWidget>] {
197 &[]
198 }
199}
200 
201// =============================================================================
202// TaffyLayoutManager
203// =============================================================================
204 
205/// Main layout manager wrapping Taffy.
206///
207/// Provides graceful degradation with cached layouts and performance monitoring.
208///
209/// # Thread Safety
210///
211/// NOT thread-safe. Must be used from the main thread only.
212///
213/// # Example
214///
215/// ```rust,ignore
216/// let mut manager = TaffyLayoutManager::new();
217/// let root_widget = build_ui_tree();
218/// let window_size = taffy::Size { width: 800.0, height: 600.0 };
219///
220/// let layout = manager.compute(&root_widget, window_size)?;
221///
222/// for cmd in layout.draw_commands() {
223/// // Render widget at cmd.viewport
224/// }
225/// ```
226pub struct TaffyLayoutManager {
227 /// Internal Taffy tree.
228 tree: TaffyTree<()>,
229 
230 /// Dirty flag for rebuild optimization.
231 is_dirty: bool,
232 
233 /// Last valid layout for graceful degradation.
234 last_valid_layout: Option<ComputedLayout>,
235 
236 /// Last window size for cache invalidation.
237 last_window_size: Option<taffy::Size<f32>>,
238 
239 /// Last root node ID for cache retrieval.
240 last_root_node: Option<NodeId>,
241 
242 /// Performance metrics (conditional compilation).
243 #[cfg(feature = "perf-metrics")]
244 metrics: PerformanceMetrics,
245}
246 
247/// Performance metrics for layout computation.
248#[cfg(feature = "perf-metrics")]
249#[derive(Debug, Clone, Default)]
250pub struct PerformanceMetrics {
251 /// Time spent in last layout computation.
252 pub layout_compute_time: Duration,
253 
254 /// Time spent rebuilding tree.
255 pub tree_rebuild_time: Duration,
256 
257 /// Time spent generating draw commands.
258 pub render_gen_time: Duration,
259}
260 
261impl TaffyLayoutManager {
262 /// Create a new layout manager.
263 ///
264 /// # Returns
265 ///
266 /// A new `TaffyLayoutManager` ready for use.
267 pub fn new() -> Self {
268 Self {
269 tree: TaffyTree::new(),
270 is_dirty: true,
271 last_valid_layout: None,
272 last_window_size: None,
273 last_root_node: None,
274 #[cfg(feature = "perf-metrics")]
275 metrics: PerformanceMetrics::default(),
276 }
277 }
278 
279 /// Mark the layout as dirty, forcing a rebuild on next compute.
280 #[inline]
281 pub fn mark_dirty(&mut self) {
282 self.is_dirty = true;
283 }
284 
285 /// Check if the layout is dirty.
286 #[inline]
287 pub fn is_dirty(&self) -> bool {
288 self.is_dirty
289 }
290 
291 /// Handle window resize event.
292 ///
293 /// # Arguments
294 ///
295 /// * `size` - New window size
296 ///
297 /// # Returns
298 ///
299 /// * `Ok(())` - If resize was handled
300 /// * `Err(TaffyLayoutError)` - If size is invalid
301 ///
302 /// # Side Effects
303 ///
304 /// Marks layout as dirty if size changed.
305 pub fn handle_resize(&mut self, size: taffy::Size<f32>) -> TaffyLayoutResult<()> {
306 // Validate size
307 if !size.width.is_finite() || !size.height.is_finite() {
308 return Err(TaffyLayoutError::InvalidWindowSize {
309 width: size.width,
310 height: size.height,
311 });
312 }
313 
314 // Check if size changed
315 if self.last_window_size != Some(size) {
316 self.last_window_size = Some(size);
317 self.is_dirty = true;
318 }
319 
320 Ok(())
321 }
322 
323 /// Compute layout for the widget tree.
324 ///
325 /// # Arguments
326 ///
327 /// * `root` - Root widget implementing TaffyWidget
328 /// * `window_size` - Available window size
329 ///
330 /// # Returns
331 ///
332 /// * `Ok(ComputedLayout)` - Computed layout with draw commands
333 /// * `Err(TaffyLayoutError)` - If computation fails and no fallback available
334 ///
335 /// # Errors
336 ///
337 /// - `TaffyLayoutError::InvalidWindowSize` - If window size is invalid
338 /// - `TaffyLayoutError::ComputationFailed` - If Taffy fails (uses fallback if available)
339 ///
340 /// # Panics
341 ///
342 /// Never panics. All errors handled with Result.
343 ///
344 /// # Performance
345 ///
346 /// WCET: < 5ms per 1000 nodes (Release mode, Ryzen 5800X).
347 pub fn compute(
348 &mut self,
349 root: &dyn TaffyWidget,
350 window_size: taffy::Size<f32>,
351 ) -> TaffyLayoutResult<(NodeId, ComputedLayout)> {
352 #[cfg(feature = "perf-metrics")]
353 let frame_start = Instant::now();
354 
355 // STEP 1: Validate window size (NEVER TRUST INPUT)
356 if !window_size.width.is_finite()
357 || !window_size.height.is_finite()
358 || window_size.width <= 0.0
359 || window_size.height <= 0.0
360 {
361 tracing::error!(
362 "Invalid window size: {}x{}",
363 window_size.width,
364 window_size.height
365 );
366 
367 // Return cached layout if available
368 if let Some(ref cached) = self.last_valid_layout {
369 if let Some(root) = self.last_root_node {
370 return Ok((root, cached.clone()));
371 }
372 }
373 
374 return Err(TaffyLayoutError::InvalidWindowSize {
375 width: window_size.width,
376 height: window_size.height,
377 });
378 }
379 
380 // STEP 2: Check dirty flag (optimization)
381 // We need to know the root node ID even if cached.
382 // We don't store the root node ID in `TaffyLayoutManager`. We should.
383 // Let's add `root_node: Option<NodeId>` to struct.
384 if !self.is_dirty && self.last_window_size == Some(window_size) {
385 if let Some(ref cached) = self.last_valid_layout {
386 if let Some(root) = self.last_root_node {
387 return Ok((root, cached.clone()));
388 }
389 }
390 }
391 
392 // STEP 3: Pre-validate widget tree
393 if let Err(e) = root.validate() {
394 tracing::error!("Widget validation failed: {:?}", e);
395 return Err(TaffyLayoutError::ComputationFailed {
396 reason: format!("Validation failed: {:?}", e),
397 });
398 }
399 
400 // STEP 4: Rebuild tree (full clear)
401 #[cfg(feature = "perf-metrics")]
402 let rebuild_start = Instant::now();
403 
404 self.tree.clear();
405 
406 let root_node = match self.build_tree(root) {
407 Ok(node) => node,
408 Err(e) => {
409 tracing::error!("Tree build failed: {:?}", e);
410 return Err(e);
411 }
412 };
413 
414 #[cfg(feature = "perf-metrics")]
415 {
416 self.metrics.tree_rebuild_time = rebuild_start.elapsed();
417 }
418 
419 // STEP 5: Compute layout (CRITICAL SECTION)
420 let available_space = taffy::Size {
421 width: AvailableSpace::Definite(window_size.width),
422 height: AvailableSpace::Definite(window_size.height),
423 };
424 
425 match self.tree.compute_layout(root_node, available_space) {
426 Ok(_) => {
427 // STEP 6: Extract computed layout
428 #[cfg(feature = "perf-metrics")]
429 let gen_start = Instant::now();
430 
431 let layout = self.extract_computed_layout(root_node)?;
432 
433 #[cfg(feature = "perf-metrics")]
434 {
435 self.metrics.render_gen_time = gen_start.elapsed();
436 }
437 
438 // Update cache
439 self.last_valid_layout = Some(layout.clone());
440 self.last_window_size = Some(window_size);
441 self.last_root_node = Some(root_node);
442 self.is_dirty = false;
443 
444 #[cfg(feature = "perf-metrics")]
445 {
446 self.metrics.layout_compute_time = frame_start.elapsed();
447 // ... metrics checks ...
448 }
449 
450 Ok((root_node, layout))
451 }
452 Err(taffy_error) => {
453 tracing::error!("Taffy computation failed: {:?}", taffy_error);
454 Err(TaffyLayoutError::ComputationFailed {
455 reason: format!("{:?}", taffy_error),
456 })
457 }
458 }
459 }
460 
461 /// Build the Taffy tree from a widget tree.
462 fn build_tree(&mut self, root: &dyn TaffyWidget) -> TaffyLayoutResult<NodeId> {
463 root.build_layout(&mut self.tree)
464 }
465 
466 /// Extract computed layout as draw commands.
467 fn extract_computed_layout(&self, root: NodeId) -> TaffyLayoutResult<ComputedLayout> {
468 let mut draw_commands = Vec::new();
469 self.traverse_tree(root, 0, &mut draw_commands)?;
470 Ok(ComputedLayout::new(draw_commands))
471 }
472 
473 /// Traverse tree depth-first, building draw commands.
474 fn traverse_tree(
475 &self,
476 node: NodeId,
477 depth: usize,
478 commands: &mut Vec<DrawCommand>,
479 ) -> TaffyLayoutResult<()> {
480 // Get layout for this node
481 let layout = self
482 .tree
483 .layout(node)
484 .map_err(|e| TaffyLayoutError::ComputationFailed {
485 reason: format!("Failed to get layout: {:?}", e),
486 })?;
487 
488 // Validate coordinates (CRITICAL)
489 let validated_viewport = ValidatedRect::from_taffy(layout).map_err(|e| {
490 tracing::error!("Invalid coordinates for node {:?}: {:?}", node, e);
491 TaffyLayoutError::CorruptedTree
492 })?;
493 
494 // Add draw command
495 commands.push(DrawCommand::new(node, validated_viewport, depth));
496 
497 // Traverse children
498 let children =
499 self.tree
500 .children(node)
501 .map_err(|e| TaffyLayoutError::ComputationFailed {
502 reason: format!("Failed to get children: {:?}", e),
503 })?;
504 
505 for child in children {
506 self.traverse_tree(child, depth + 1, commands)?;
507 }
508 
509 Ok(())
510 }
511 
512 /// Get performance metrics (only available with `perf-metrics` feature).
513 #[cfg(feature = "perf-metrics")]
514 pub fn metrics(&self) -> &PerformanceMetrics {
515 &self.metrics
516 }
517 
518 /// Get the internal Taffy tree for advanced use cases.
519 ///
520 /// # Warning
521 ///
522 /// Direct tree manipulation may invalidate cached layouts.
523 pub fn tree(&self) -> &TaffyTree<()> {
524 &self.tree
525 }
526 
527 /// Get mutable access to the internal Taffy tree.
528 ///
529 /// # Warning
530 ///
531 /// Direct tree manipulation invalidates cached layouts.
532 /// Call `mark_dirty()` after modifications.
533 pub fn tree_mut(&mut self) -> &mut TaffyTree<()> {
534 self.is_dirty = true;
535 &mut self.tree
536 }
537}
538 
539impl Default for TaffyLayoutManager {
540 fn default() -> Self {
541 Self::new()
542 }
543}
544 
545impl std::fmt::Debug for TaffyLayoutManager {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 f.debug_struct("TaffyLayoutManager")
548 .field("is_dirty", &self.is_dirty)
549 .field("has_cached_layout", &self.last_valid_layout.is_some())
550 .field("last_window_size", &self.last_window_size)
551 .finish()
552 }
553}
554 
555// =============================================================================
556// Helper Functions
557// =============================================================================
558 
559/// Convert EdgeInsets to Taffy Rect for padding/margin.
560pub fn edge_insets_to_taffy(insets: &EdgeInsets) -> taffy::Rect<LengthPercentage> {
561 taffy::Rect {
562 left: length(insets.left),
563 right: length(insets.right),
564 top: length(insets.top),
565 bottom: length(insets.bottom),
566 }
567}
568 
569/// Validate EdgeInsets.
570///
571/// # Returns
572///
573/// * `Ok(())` - If all values are finite and non-negative
574/// * `Err(TaffyValidationError)` - If validation fails
575pub fn validate_edge_insets(insets: &EdgeInsets) -> TaffyValidationResult<()> {
576 if !insets.left.is_finite()
577 || !insets.right.is_finite()
578 || !insets.top.is_finite()
579 || !insets.bottom.is_finite()
580 {
581 return Err(TaffyValidationError::NonFiniteValue);
582 }
583 
584 if insets.left < 0.0 || insets.right < 0.0 || insets.top < 0.0 || insets.bottom < 0.0 {
585 return Err(TaffyValidationError::InvalidPadding);
586 }
587 
588 Ok(())
589}
590 
591// =============================================================================
592// Tests
593// =============================================================================
594 
595#[cfg(test)]
596mod tests {
597 use super::*;
598 
599 /// Simple test widget for testing.
600 #[derive(Debug)]
601 struct TestWidget {
602 width: f32,
603 height: f32,
604 }
605 
606 impl TestWidget {
607 fn new(width: f32, height: f32) -> Self {
608 Self { width, height }
609 }
610 }
611 
612 impl TaffyWidget for TestWidget {
613 fn build_layout(&self, tree: &mut TaffyTree<()>) -> TaffyLayoutResult<NodeId> {
614 let style = Style {
615 size: Size {
616 width: length(self.width),
617 height: length(self.height),
618 },
619 ..Default::default()
620 };
621 tree.new_leaf(style).map_err(Into::into)
622 }
623 }
624 
625 #[test]
626 fn test_manager_new() {
627 let manager = TaffyLayoutManager::new();
628 assert!(manager.is_dirty());
629 assert!(manager.last_valid_layout.is_none());
630 }
631 
632 #[test]
633 fn test_compute_basic() {
634 let mut manager = TaffyLayoutManager::new();
635 let widget = TestWidget::new(100.0, 50.0);
636 let size = taffy::Size {
637 width: 800.0,
638 height: 600.0,
639 };
640 
641 let result = manager.compute(&widget, size);
642 assert!(result.is_ok());
643 
644 let output = result.unwrap();
645 let layout = output.1;
646 assert_eq!(layout.len(), 1);
647 assert!(!manager.is_dirty());
648 }
649 
650 #[test]
651 fn test_compute_survives_invalid_window_size() {
652 let mut manager = TaffyLayoutManager::new();
653 let widget = TestWidget::new(100.0, 50.0);
654 
655 // First compute a valid layout
656 let valid_size = taffy::Size {
657 width: 800.0,
658 height: 600.0,
659 };
660 let _ = manager.compute(&widget, valid_size);
661 
662 // Now try with invalid size
663 let invalid_size = taffy::Size {
664 width: 0.0,
665 height: 0.0,
666 };
667 let result = manager.compute(&widget, invalid_size);
668 
669 // Should return cached layout, not panic
670 assert!(result.is_ok());
671 }
672 
673 #[test]
674 fn test_first_frame_invalid_size_returns_err() {
675 let mut manager = TaffyLayoutManager::new();
676 let widget = TestWidget::new(100.0, 50.0);
677 
678 // First frame with invalid size - no fallback available
679 let invalid_size = taffy::Size {
680 width: 0.0,
681 height: 0.0,
682 };
683 let result = manager.compute(&widget, invalid_size);
684 
685 assert!(result.is_err());
686 }
687 
688 #[test]
689 fn test_dirty_flag_prevents_recompute() {
690 let mut manager = TaffyLayoutManager::new();
691 let widget = TestWidget::new(100.0, 50.0);
692 let size = taffy::Size {
693 width: 800.0,
694 height: 600.0,
695 };
696 
697 // First compute
698 let _ = manager.compute(&widget, size);
699 assert!(!manager.is_dirty());
700 
701 // Second compute should use cache
702 let result = manager.compute(&widget, size);
703 assert!(result.is_ok());
704 }
705 
706 #[test]
707 fn test_resize_marks_dirty() {
708 let mut manager = TaffyLayoutManager::new();
709 let widget = TestWidget::new(100.0, 50.0);
710 
711 // First compute
712 let size1 = taffy::Size {
713 width: 800.0,
714 height: 600.0,
715 };
716 let _ = manager.compute(&widget, size1);
717 assert!(!manager.is_dirty());
718 
719 // Resize
720 let size2 = taffy::Size {
721 width: 1024.0,
722 height: 768.0,
723 };
724 let _ = manager.handle_resize(size2);
725 assert!(manager.is_dirty());
726 }
727 
728 #[test]
729 fn test_handle_resize_invalid() {
730 let mut manager = TaffyLayoutManager::new();
731 
732 let invalid_size = taffy::Size {
733 width: f32::NAN,
734 height: 600.0,
735 };
736 let result = manager.handle_resize(invalid_size);
737 
738 assert!(result.is_err());
739 }
740 
741 #[test]
742 fn test_computed_layout_default() {
743 let layout = ComputedLayout::default();
744 assert!(layout.is_empty());
745 assert_eq!(layout.len(), 0);
746 }
747 
748 #[test]
749 fn test_validate_edge_insets_valid() {
750 let insets = EdgeInsets {
751 top: 10.0,
752 right: 20.0,
753 bottom: 10.0,
754 left: 20.0,
755 };
756 assert!(validate_edge_insets(&insets).is_ok());
757 }
758 
759 #[test]
760 fn test_validate_edge_insets_negative() {
761 let insets = EdgeInsets {
762 top: -10.0,
763 right: 20.0,
764 bottom: 10.0,
765 left: 20.0,
766 };
767 assert!(validate_edge_insets(&insets).is_err());
768 }
769 
770 #[test]
771 fn test_validate_edge_insets_nan() {
772 let insets = EdgeInsets {
773 top: f32::NAN,
774 right: 20.0,
775 bottom: 10.0,
776 left: 20.0,
777 };
778 assert!(validate_edge_insets(&insets).is_err());
779 }
780}
781