Skip to main content

blinc_layout/
element.rs

1//! Element types and traits for layout-driven UI
2//!
3//! Provides the core abstractions for building layout trees that can be
4//! rendered via the DrawContext API.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use blinc_core::{
10    BlurQuality, Brush, ClipPath, Color, CornerRadius, CornerShape, DynFloat, DynValue, FlowGraph,
11    LayerEffect, OverflowFade, Rect, Shadow, Transform, ValueContext,
12};
13use taffy::Layout;
14
15use crate::tree::LayoutNodeId;
16
17// =============================================================================
18// FlowRef — polymorphic flow reference (name string or direct FlowGraph)
19// =============================================================================
20
21/// A reference to a `@flow` shader — either by name (for CSS-defined flows) or
22/// by direct `FlowGraph` (for `flow!` macro-defined flows).
23#[derive(Clone, Debug)]
24pub enum FlowRef {
25    /// Name of a @flow DAG defined in a CSS stylesheet
26    Name(String),
27    /// Direct FlowGraph (e.g. from `flow!` macro), auto-persisted by name
28    Graph(Arc<FlowGraph>),
29}
30
31impl From<&str> for FlowRef {
32    fn from(s: &str) -> Self {
33        FlowRef::Name(s.to_string())
34    }
35}
36
37impl From<String> for FlowRef {
38    fn from(s: String) -> Self {
39        FlowRef::Name(s)
40    }
41}
42
43impl From<FlowGraph> for FlowRef {
44    fn from(g: FlowGraph) -> Self {
45        FlowRef::Graph(Arc::new(g))
46    }
47}
48
49/// Per-tag SVG style overrides for CSS tag-name selectors (e.g., `path { fill: red; }`)
50#[derive(Clone, Debug, Default, PartialEq)]
51pub struct SvgTagStyle {
52    pub fill: Option<[f32; 4]>,
53    pub stroke: Option<[f32; 4]>,
54    pub stroke_width: Option<f32>,
55    pub stroke_dasharray: Option<Vec<f32>>,
56    pub stroke_dashoffset: Option<f32>,
57    pub opacity: Option<f32>,
58}
59
60// ============================================================================
61// Cursor Style
62// ============================================================================
63
64/// Mouse cursor style for an element
65///
66/// When the cursor hovers over an element with a cursor style set,
67/// the window cursor will change to this style.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69pub enum CursorStyle {
70    /// Default arrow cursor
71    #[default]
72    Default,
73    /// Pointer/hand cursor (for clickable elements like links, buttons)
74    Pointer,
75    /// Text/I-beam cursor (for text input)
76    Text,
77    /// Crosshair cursor
78    Crosshair,
79    /// Move cursor (for dragging)
80    Move,
81    /// Not allowed cursor
82    NotAllowed,
83    /// North-South resize cursor
84    ResizeNS,
85    /// East-West resize cursor
86    ResizeEW,
87    /// Northeast-Southwest resize cursor
88    ResizeNESW,
89    /// Northwest-Southeast resize cursor
90    ResizeNWSE,
91    /// Grab cursor (open hand)
92    Grab,
93    /// Grabbing cursor (closed hand)
94    Grabbing,
95    /// Wait/loading cursor
96    Wait,
97    /// Progress cursor (arrow with spinner)
98    Progress,
99    /// Hidden cursor
100    None,
101}
102
103// ============================================================================
104// Material System
105// ============================================================================
106
107/// Material types that can be applied to elements
108///
109/// Materials define how an element appears and interacts with its background.
110/// This is similar to a physical material system where each material has
111/// unique visual properties.
112#[derive(Clone, Debug)]
113pub enum Material {
114    /// Transparent glass/vibrancy effect that blurs content behind it
115    Glass(GlassMaterial),
116    /// Metallic/reflective surface
117    Metallic(MetallicMaterial),
118    /// Wood grain texture (placeholder for future implementation)
119    Wood(WoodMaterial),
120    /// Solid opaque material (default)
121    Solid(SolidMaterial),
122}
123
124impl Default for Material {
125    fn default() -> Self {
126        Material::Solid(SolidMaterial::default())
127    }
128}
129
130// ============================================================================
131// Into<Material> implementations for ergonomic effect() API
132// ============================================================================
133
134impl From<GlassMaterial> for Material {
135    fn from(glass: GlassMaterial) -> Self {
136        Material::Glass(glass)
137    }
138}
139
140impl From<MetallicMaterial> for Material {
141    fn from(metal: MetallicMaterial) -> Self {
142        Material::Metallic(metal)
143    }
144}
145
146impl From<WoodMaterial> for Material {
147    fn from(wood: WoodMaterial) -> Self {
148        Material::Wood(wood)
149    }
150}
151
152impl From<SolidMaterial> for Material {
153    fn from(solid: SolidMaterial) -> Self {
154        Material::Solid(solid)
155    }
156}
157
158// ============================================================================
159// Glass Material
160// ============================================================================
161
162/// Glass/vibrancy material that blurs content behind it
163///
164/// Creates a frosted glass effect similar to macOS vibrancy or iOS blur.
165#[derive(Clone, Debug)]
166pub struct GlassMaterial {
167    /// Blur intensity (0-50, default 20)
168    pub blur: f32,
169    /// Tint color applied over the blur
170    pub tint: Color,
171    /// Color saturation (1.0 = normal, 0.0 = grayscale, >1.0 = vibrant)
172    pub saturation: f32,
173    /// Brightness multiplier (1.0 = normal)
174    pub brightness: f32,
175    /// Noise/grain amount for frosted texture (0.0-0.1)
176    pub noise: f32,
177    /// Border highlight thickness
178    pub border_thickness: f32,
179    /// Optional drop shadow
180    pub shadow: Option<MaterialShadow>,
181    /// Use simple frosted glass mode (no liquid glass effects)
182    pub simple: bool,
183}
184
185impl Default for GlassMaterial {
186    fn default() -> Self {
187        Self {
188            blur: 20.0,
189            tint: Color::rgba(1.0, 1.0, 1.0, 0.1),
190            saturation: 1.0,
191            brightness: 1.0,
192            noise: 0.0,
193            border_thickness: 0.8,
194            shadow: None,
195            simple: false,
196        }
197    }
198}
199
200impl GlassMaterial {
201    /// Create a new glass material with default settings
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    /// Set blur intensity
207    pub fn blur(mut self, blur: f32) -> Self {
208        self.blur = blur;
209        self
210    }
211
212    /// Set tint color
213    pub fn tint(mut self, color: Color) -> Self {
214        self.tint = color;
215        self
216    }
217
218    /// Set tint from RGBA components
219    pub fn tint_rgba(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
220        self.tint = Color::rgba(r, g, b, a);
221        self
222    }
223
224    /// Set saturation
225    pub fn saturation(mut self, saturation: f32) -> Self {
226        self.saturation = saturation;
227        self
228    }
229
230    /// Set brightness
231    pub fn brightness(mut self, brightness: f32) -> Self {
232        self.brightness = brightness;
233        self
234    }
235
236    /// Add noise/grain for frosted effect
237    pub fn noise(mut self, amount: f32) -> Self {
238        self.noise = amount;
239        self
240    }
241
242    /// Set border highlight thickness
243    pub fn border(mut self, thickness: f32) -> Self {
244        self.border_thickness = thickness;
245        self
246    }
247
248    /// Add drop shadow
249    pub fn shadow(mut self, shadow: MaterialShadow) -> Self {
250        self.shadow = Some(shadow);
251        self
252    }
253
254    // Presets
255
256    /// Ultra-thin glass (very subtle blur)
257    pub fn ultra_thin() -> Self {
258        Self::new().blur(10.0)
259    }
260
261    /// Thin glass
262    pub fn thin() -> Self {
263        Self::new().blur(15.0)
264    }
265
266    /// Regular glass (default blur)
267    pub fn regular() -> Self {
268        Self::new()
269    }
270
271    /// Thick glass (heavy blur)
272    pub fn thick() -> Self {
273        Self::new().blur(30.0)
274    }
275
276    /// Frosted glass with grain texture
277    pub fn frosted() -> Self {
278        Self::new().noise(0.03)
279    }
280
281    /// Card style with border and shadow
282    pub fn card() -> Self {
283        Self::new().border(1.0).shadow(MaterialShadow::md())
284    }
285
286    /// Simple frosted glass - pure blur without liquid glass effects
287    ///
288    /// Creates a clean backdrop blur effect without edge refraction,
289    /// light reflections, or bevel effects. More performant and ideal
290    /// for subtle UI backgrounds where you want pure frosted glass.
291    pub fn simple() -> Self {
292        Self {
293            blur: 15.0,
294            tint: Color::rgba(1.0, 1.0, 1.0, 0.15),
295            saturation: 1.1,
296            brightness: 1.0,
297            noise: 0.0,
298            border_thickness: 0.0,
299            shadow: None,
300            simple: true,
301        }
302    }
303
304    /// Enable/disable simple mode (no liquid glass effects)
305    pub fn with_simple(mut self, simple: bool) -> Self {
306        self.simple = simple;
307        self
308    }
309}
310
311// ============================================================================
312// Metallic Material
313// ============================================================================
314
315/// Metallic/reflective material
316///
317/// Creates a metallic appearance with highlights and reflections.
318#[derive(Clone, Debug)]
319pub struct MetallicMaterial {
320    /// Base metal color
321    pub color: Color,
322    /// Roughness (0.0 = mirror, 1.0 = matte)
323    pub roughness: f32,
324    /// Metallic intensity (0.0 = dielectric, 1.0 = full metal)
325    pub metallic: f32,
326    /// Reflection intensity
327    pub reflection: f32,
328    /// Optional drop shadow
329    pub shadow: Option<MaterialShadow>,
330}
331
332impl Default for MetallicMaterial {
333    fn default() -> Self {
334        Self {
335            color: Color::rgba(0.8, 0.8, 0.85, 1.0),
336            roughness: 0.3,
337            metallic: 1.0,
338            reflection: 0.5,
339            shadow: None,
340        }
341    }
342}
343
344impl MetallicMaterial {
345    /// Create a new metallic material
346    pub fn new() -> Self {
347        Self::default()
348    }
349
350    /// Set base color
351    pub fn color(mut self, color: Color) -> Self {
352        self.color = color;
353        self
354    }
355
356    /// Set roughness (0 = mirror, 1 = matte)
357    pub fn roughness(mut self, roughness: f32) -> Self {
358        self.roughness = roughness;
359        self
360    }
361
362    /// Set metallic intensity
363    pub fn metallic(mut self, metallic: f32) -> Self {
364        self.metallic = metallic;
365        self
366    }
367
368    /// Set reflection intensity
369    pub fn reflection(mut self, reflection: f32) -> Self {
370        self.reflection = reflection;
371        self
372    }
373
374    /// Add drop shadow
375    pub fn shadow(mut self, shadow: MaterialShadow) -> Self {
376        self.shadow = Some(shadow);
377        self
378    }
379
380    // Presets
381
382    /// Chrome/mirror finish
383    pub fn chrome() -> Self {
384        Self::new().roughness(0.1).reflection(0.8)
385    }
386
387    /// Brushed metal
388    pub fn brushed() -> Self {
389        Self::new().roughness(0.5).reflection(0.3)
390    }
391
392    /// Gold finish
393    pub fn gold() -> Self {
394        Self::new()
395            .color(Color::rgba(1.0, 0.84, 0.0, 1.0))
396            .roughness(0.2)
397    }
398
399    /// Silver finish
400    pub fn silver() -> Self {
401        Self::new()
402            .color(Color::rgba(0.75, 0.75, 0.75, 1.0))
403            .roughness(0.2)
404    }
405
406    /// Copper finish
407    pub fn copper() -> Self {
408        Self::new()
409            .color(Color::rgba(0.72, 0.45, 0.2, 1.0))
410            .roughness(0.3)
411    }
412}
413
414// ============================================================================
415// Wood Material (placeholder)
416// ============================================================================
417
418/// Wood grain material (placeholder for future texture support)
419#[derive(Clone, Debug)]
420pub struct WoodMaterial {
421    /// Base wood color
422    pub color: Color,
423    /// Grain intensity
424    pub grain: f32,
425    /// Glossiness (0 = matte, 1 = polished)
426    pub gloss: f32,
427    /// Optional drop shadow
428    pub shadow: Option<MaterialShadow>,
429}
430
431impl Default for WoodMaterial {
432    fn default() -> Self {
433        Self {
434            color: Color::rgba(0.55, 0.35, 0.2, 1.0),
435            grain: 0.5,
436            gloss: 0.2,
437            shadow: None,
438        }
439    }
440}
441
442impl WoodMaterial {
443    /// Create a new wood material
444    pub fn new() -> Self {
445        Self::default()
446    }
447
448    /// Set base color
449    pub fn color(mut self, color: Color) -> Self {
450        self.color = color;
451        self
452    }
453
454    /// Set grain intensity
455    pub fn grain(mut self, grain: f32) -> Self {
456        self.grain = grain;
457        self
458    }
459
460    /// Set glossiness
461    pub fn gloss(mut self, gloss: f32) -> Self {
462        self.gloss = gloss;
463        self
464    }
465
466    /// Add drop shadow
467    pub fn shadow(mut self, shadow: MaterialShadow) -> Self {
468        self.shadow = Some(shadow);
469        self
470    }
471
472    // Presets
473
474    /// Oak wood
475    pub fn oak() -> Self {
476        Self::new().color(Color::rgba(0.6, 0.45, 0.25, 1.0))
477    }
478
479    /// Walnut wood
480    pub fn walnut() -> Self {
481        Self::new().color(Color::rgba(0.4, 0.25, 0.15, 1.0))
482    }
483
484    /// Cherry wood
485    pub fn cherry() -> Self {
486        Self::new().color(Color::rgba(0.6, 0.3, 0.2, 1.0))
487    }
488
489    /// Pine wood (lighter)
490    pub fn pine() -> Self {
491        Self::new().color(Color::rgba(0.8, 0.65, 0.45, 1.0))
492    }
493}
494
495// ============================================================================
496// Solid Material
497// ============================================================================
498
499/// Solid opaque material (the default)
500#[derive(Clone, Debug, Default)]
501pub struct SolidMaterial {
502    /// Optional drop shadow
503    pub shadow: Option<MaterialShadow>,
504}
505
506impl SolidMaterial {
507    /// Create a new solid material
508    pub fn new() -> Self {
509        Self::default()
510    }
511
512    /// Add drop shadow
513    pub fn shadow(mut self, shadow: MaterialShadow) -> Self {
514        self.shadow = Some(shadow);
515        self
516    }
517}
518
519// ============================================================================
520// Material Shadow
521// ============================================================================
522
523/// Shadow configuration for materials
524#[derive(Clone, Debug)]
525pub struct MaterialShadow {
526    /// Shadow color
527    pub color: Color,
528    /// Blur radius
529    pub blur: f32,
530    /// Offset (x, y)
531    pub offset: (f32, f32),
532    /// Opacity
533    pub opacity: f32,
534}
535
536impl Default for MaterialShadow {
537    fn default() -> Self {
538        Self {
539            color: Color::rgba(0.0, 0.0, 0.0, 1.0),
540            blur: 10.0,
541            offset: (0.0, 4.0),
542            opacity: 0.25,
543        }
544    }
545}
546
547/// Convert from blinc_core::Shadow to MaterialShadow
548impl From<Shadow> for MaterialShadow {
549    fn from(shadow: Shadow) -> Self {
550        Self {
551            color: shadow.color,
552            blur: shadow.blur,
553            offset: (shadow.offset_x, shadow.offset_y),
554            // Use the shadow color's alpha as opacity
555            opacity: shadow.color.a,
556        }
557    }
558}
559
560/// Convert from &Shadow to MaterialShadow
561impl From<&Shadow> for MaterialShadow {
562    fn from(shadow: &Shadow) -> Self {
563        Self {
564            color: shadow.color,
565            blur: shadow.blur,
566            offset: (shadow.offset_x, shadow.offset_y),
567            opacity: shadow.color.a,
568        }
569    }
570}
571
572impl MaterialShadow {
573    /// Create a new shadow
574    pub fn new() -> Self {
575        Self::default()
576    }
577
578    /// Set shadow color
579    pub fn color(mut self, color: Color) -> Self {
580        self.color = color;
581        self
582    }
583
584    /// Set blur radius
585    pub fn blur(mut self, blur: f32) -> Self {
586        self.blur = blur;
587        self
588    }
589
590    /// Set offset
591    pub fn offset(mut self, x: f32, y: f32) -> Self {
592        self.offset = (x, y);
593        self
594    }
595
596    /// Set opacity
597    pub fn opacity(mut self, opacity: f32) -> Self {
598        self.opacity = opacity;
599        self
600    }
601
602    // Presets
603
604    /// Small shadow
605    pub fn sm() -> Self {
606        Self::new().blur(4.0).offset(0.0, 2.0).opacity(0.2)
607    }
608
609    /// Medium shadow
610    pub fn md() -> Self {
611        Self::new().blur(10.0).offset(0.0, 4.0).opacity(0.25)
612    }
613
614    /// Large shadow
615    pub fn lg() -> Self {
616        Self::new().blur(20.0).offset(0.0, 8.0).opacity(0.3)
617    }
618
619    /// Extra large shadow
620    pub fn xl() -> Self {
621        Self::new().blur(30.0).offset(0.0, 12.0).opacity(0.35)
622    }
623}
624
625/// Computed layout bounds for an element after layout computation
626#[derive(Clone, Copy, Debug, Default)]
627pub struct ElementBounds {
628    /// X position relative to parent
629    pub x: f32,
630    /// Y position relative to parent
631    pub y: f32,
632    /// Computed width
633    pub width: f32,
634    /// Computed height
635    pub height: f32,
636}
637
638impl ElementBounds {
639    /// Create bounds from a Taffy Layout with parent offset
640    pub fn from_layout(layout: &Layout, parent_offset: (f32, f32)) -> Self {
641        Self {
642            x: parent_offset.0 + layout.location.x,
643            y: parent_offset.1 + layout.location.y,
644            width: layout.size.width,
645            height: layout.size.height,
646        }
647    }
648
649    /// Create bounds at origin with given size
650    pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
651        Self {
652            x,
653            y,
654            width,
655            height,
656        }
657    }
658
659    /// Convert to a blinc_core Rect
660    pub fn to_rect(&self) -> Rect {
661        Rect::new(self.x, self.y, self.width, self.height)
662    }
663
664    /// Get bounds relative to self (origin at 0,0)
665    pub fn local(&self) -> Self {
666        Self {
667            x: 0.0,
668            y: 0.0,
669            width: self.width,
670            height: self.height,
671        }
672    }
673}
674
675/// Render layer for separating elements in glass-effect rendering
676///
677/// When using glass/vibrancy effects, elements need to be rendered in
678/// different passes:
679/// - Background elements are rendered first and get blurred behind glass
680/// - Glass elements render the glass effect itself
681/// - Foreground elements render on top without being blurred
682#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
683pub enum RenderLayer {
684    /// Rendered behind glass (will be blurred)
685    #[default]
686    Background,
687    /// Rendered as a glass element (blur effect applied)
688    Glass,
689    /// Rendered on top of glass (not blurred)
690    Foreground,
691}
692
693/// Motion animation configuration for enter/exit animations
694#[derive(Clone, Debug, Default)]
695pub struct MotionAnimation {
696    /// Enter animation properties (opacity, scale, translate at t=0)
697    pub enter_from: Option<MotionKeyframe>,
698    /// Enter animation duration in ms
699    pub enter_duration_ms: u32,
700    /// Enter animation delay in ms (for stagger)
701    pub enter_delay_ms: u32,
702    /// Exit animation properties (opacity, scale, translate at t=1)
703    pub exit_to: Option<MotionKeyframe>,
704    /// Exit animation duration in ms
705    pub exit_duration_ms: u32,
706}
707
708/// A single keyframe of motion animation values
709#[derive(Clone, Debug, Default)]
710pub struct MotionKeyframe {
711    /// Opacity (0.0 - 1.0)
712    pub opacity: Option<f32>,
713    /// Scale X
714    pub scale_x: Option<f32>,
715    /// Scale Y
716    pub scale_y: Option<f32>,
717    /// Translate X in pixels
718    pub translate_x: Option<f32>,
719    /// Translate Y in pixels
720    pub translate_y: Option<f32>,
721    /// Rotation in degrees
722    pub rotate: Option<f32>,
723}
724
725impl MotionKeyframe {
726    /// Create a new empty keyframe
727    pub fn new() -> Self {
728        Self::default()
729    }
730
731    /// Set opacity
732    pub fn opacity(mut self, opacity: f32) -> Self {
733        self.opacity = Some(opacity);
734        self
735    }
736
737    /// Set uniform scale
738    pub fn scale(mut self, scale: f32) -> Self {
739        self.scale_x = Some(scale);
740        self.scale_y = Some(scale);
741        self
742    }
743
744    /// Set translation
745    pub fn translate(mut self, x: f32, y: f32) -> Self {
746        self.translate_x = Some(x);
747        self.translate_y = Some(y);
748        self
749    }
750
751    /// Set rotation in degrees
752    pub fn rotate(mut self, degrees: f32) -> Self {
753        self.rotate = Some(degrees);
754        self
755    }
756
757    /// Create from blinc_animation KeyframeProperties
758    pub fn from_keyframe_properties(props: &blinc_animation::KeyframeProperties) -> Self {
759        Self {
760            opacity: props.opacity,
761            scale_x: props.scale_x,
762            scale_y: props.scale_y,
763            translate_x: props.translate_x,
764            translate_y: props.translate_y,
765            rotate: props.rotate,
766        }
767    }
768
769    /// Interpolate between two keyframes
770    ///
771    /// When a property is Some in one keyframe but None in the other,
772    /// we use the "identity" value (1.0 for opacity/scale, 0.0 for translate/rotate).
773    pub fn lerp(&self, other: &Self, t: f32) -> Self {
774        Self {
775            // Opacity: identity is 1.0
776            opacity: lerp_opt_with_default(self.opacity, other.opacity, t, 1.0),
777            // Scale: identity is 1.0
778            scale_x: lerp_opt_with_default(self.scale_x, other.scale_x, t, 1.0),
779            scale_y: lerp_opt_with_default(self.scale_y, other.scale_y, t, 1.0),
780            // Translation: identity is 0.0
781            translate_x: lerp_opt_with_default(self.translate_x, other.translate_x, t, 0.0),
782            translate_y: lerp_opt_with_default(self.translate_y, other.translate_y, t, 0.0),
783            // Rotation: identity is 0.0
784            rotate: lerp_opt_with_default(self.rotate, other.rotate, t, 0.0),
785        }
786    }
787
788    /// Get the resolved opacity (defaults to 1.0 if not set)
789    pub fn resolved_opacity(&self) -> f32 {
790        self.opacity.unwrap_or(1.0)
791    }
792
793    /// Get the resolved scale (defaults to 1.0 if not set)
794    pub fn resolved_scale(&self) -> (f32, f32) {
795        (self.scale_x.unwrap_or(1.0), self.scale_y.unwrap_or(1.0))
796    }
797
798    /// Get the resolved translation (defaults to 0.0 if not set)
799    pub fn resolved_translate(&self) -> (f32, f32) {
800        (
801            self.translate_x.unwrap_or(0.0),
802            self.translate_y.unwrap_or(0.0),
803        )
804    }
805
806    /// Get the resolved rotation (defaults to 0.0 if not set)
807    pub fn resolved_rotate(&self) -> f32 {
808        self.rotate.unwrap_or(0.0)
809    }
810}
811
812/// Helper to interpolate optional values with a default for missing values
813fn lerp_opt_with_default(a: Option<f32>, b: Option<f32>, t: f32, default: f32) -> Option<f32> {
814    match (a, b) {
815        (Some(a), Some(b)) => Some(a + (b - a) * t),
816        (Some(a), None) => Some(a + (default - a) * t), // Lerp toward default
817        (None, Some(b)) => Some(default + (b - default) * t), // Lerp from default
818        (None, None) => None,
819    }
820}
821
822impl MotionAnimation {
823    /// Create a motion animation from a MultiKeyframeAnimation (enter animation)
824    pub fn from_enter_animation(
825        anim: &blinc_animation::MultiKeyframeAnimation,
826        delay_ms: u32,
827    ) -> Self {
828        // Get the first keyframe's properties as the "from" state
829        let enter_from = anim
830            .first_keyframe()
831            .map(|kf| MotionKeyframe::from_keyframe_properties(&kf.properties));
832
833        Self {
834            enter_from,
835            enter_duration_ms: anim.duration_ms(),
836            enter_delay_ms: delay_ms,
837            exit_to: None,
838            exit_duration_ms: 0,
839        }
840    }
841
842    /// Add exit animation
843    pub fn with_exit_animation(mut self, anim: &blinc_animation::MultiKeyframeAnimation) -> Self {
844        // Get the last keyframe's properties as the "to" state
845        self.exit_to = anim
846            .last_keyframe()
847            .map(|kf| MotionKeyframe::from_keyframe_properties(&kf.properties));
848        self.exit_duration_ms = anim.duration_ms();
849        self
850    }
851}
852
853/// Individual border side configuration
854#[derive(Clone, Copy, Debug, Default)]
855pub struct BorderSide {
856    /// Width in pixels (0 = no border)
857    pub width: f32,
858    /// Border color
859    pub color: Color,
860}
861
862impl BorderSide {
863    /// Create a new border side
864    pub fn new(width: f32, color: Color) -> Self {
865        Self { width, color }
866    }
867
868    /// Check if this border side is visible
869    pub fn is_visible(&self) -> bool {
870        self.width > 0.0 && self.color.a > 0.0
871    }
872}
873
874/// Per-side border configuration for CSS-like border control
875///
876/// Allows setting borders independently for each side (top, right, bottom, left).
877/// This is useful for blockquotes (left border only), dividers, etc.
878#[derive(Clone, Copy, Debug, Default)]
879pub struct BorderSides {
880    /// Top border
881    pub top: Option<BorderSide>,
882    /// Right border
883    pub right: Option<BorderSide>,
884    /// Bottom border
885    pub bottom: Option<BorderSide>,
886    /// Left border
887    pub left: Option<BorderSide>,
888}
889
890impl BorderSides {
891    /// Create empty border sides (no borders)
892    pub fn none() -> Self {
893        Self::default()
894    }
895
896    /// Check if any border side is set
897    pub fn has_any(&self) -> bool {
898        self.top.as_ref().is_some_and(|b| b.is_visible())
899            || self.right.as_ref().is_some_and(|b| b.is_visible())
900            || self.bottom.as_ref().is_some_and(|b| b.is_visible())
901            || self.left.as_ref().is_some_and(|b| b.is_visible())
902    }
903}
904
905/// Builder for constructing per-side borders with a fluent API
906///
907/// # Example
908///
909/// ```ignore
910/// use blinc_layout::element::BorderBuilder;
911/// use blinc_core::Color;
912///
913/// // Create borders with different sides
914/// let borders = BorderBuilder::new()
915///     .left(4.0, Color::BLUE)
916///     .bottom(1.0, Color::gray(0.3))
917///     .build();
918///
919/// // Or use the shorthand on Div
920/// div().borders(|b| b.left(4.0, Color::BLUE).bottom(1.0, Color::gray(0.3)))
921/// ```
922#[derive(Clone, Copy, Debug, Default)]
923pub struct BorderBuilder {
924    sides: BorderSides,
925}
926
927impl BorderBuilder {
928    /// Create a new border builder with no borders
929    pub fn new() -> Self {
930        Self::default()
931    }
932
933    /// Set the left border
934    pub fn left(mut self, width: f32, color: Color) -> Self {
935        self.sides.left = Some(BorderSide::new(width, color));
936        self
937    }
938
939    /// Set the right border
940    pub fn right(mut self, width: f32, color: Color) -> Self {
941        self.sides.right = Some(BorderSide::new(width, color));
942        self
943    }
944
945    /// Set the top border
946    pub fn top(mut self, width: f32, color: Color) -> Self {
947        self.sides.top = Some(BorderSide::new(width, color));
948        self
949    }
950
951    /// Set the bottom border
952    pub fn bottom(mut self, width: f32, color: Color) -> Self {
953        self.sides.bottom = Some(BorderSide::new(width, color));
954        self
955    }
956
957    /// Set horizontal borders (left and right)
958    pub fn x(mut self, width: f32, color: Color) -> Self {
959        let side = BorderSide::new(width, color);
960        self.sides.left = Some(side);
961        self.sides.right = Some(side);
962        self
963    }
964
965    /// Set vertical borders (top and bottom)
966    pub fn y(mut self, width: f32, color: Color) -> Self {
967        let side = BorderSide::new(width, color);
968        self.sides.top = Some(side);
969        self.sides.bottom = Some(side);
970        self
971    }
972
973    /// Set all borders to the same value
974    pub fn all(mut self, width: f32, color: Color) -> Self {
975        let side = BorderSide::new(width, color);
976        self.sides.top = Some(side);
977        self.sides.right = Some(side);
978        self.sides.bottom = Some(side);
979        self.sides.left = Some(side);
980        self
981    }
982
983    /// Build the BorderSides configuration
984    pub fn build(self) -> BorderSides {
985        self.sides
986    }
987}
988
989/// Visual properties for rendering an element
990#[derive(Clone)]
991pub struct RenderProps {
992    /// Background fill (solid color or gradient)
993    pub background: Option<Brush>,
994    /// Corner radius for rounded rectangles
995    pub border_radius: CornerRadius,
996    /// Tracks whether border_radius was explicitly set (distinguishes "set to 0" from "not set" in merges)
997    pub border_radius_explicit: bool,
998    /// Corner shape (superellipse n parameter per corner). Default is ROUND (n=1.0).
999    pub corner_shape: CornerShape,
1000    /// Border color (None = no border) - used for uniform borders
1001    pub border_color: Option<Color>,
1002    /// Border width in pixels - used for uniform borders
1003    pub border_width: f32,
1004    /// Per-side borders (takes precedence over uniform border if set)
1005    pub border_sides: BorderSides,
1006    /// Outline color (None = no outline)
1007    pub outline_color: Option<Color>,
1008    /// Outline width in pixels
1009    pub outline_width: f32,
1010    /// Outline offset in pixels (gap between border and outline)
1011    pub outline_offset: f32,
1012    /// Which layer this element renders in
1013    pub layer: RenderLayer,
1014    /// Material applied to this element (glass, metallic, etc.)
1015    pub material: Option<Material>,
1016    /// Node ID for looking up children
1017    pub node_id: Option<LayoutNodeId>,
1018    /// Drop shadow applied to this element
1019    pub shadow: Option<Shadow>,
1020    /// Transform applied to this element (translate, scale, rotate)
1021    pub transform: Option<Transform>,
1022    /// Opacity (0.0 = transparent, 1.0 = opaque)
1023    pub opacity: f32,
1024    /// Whether this element clips its children (for scroll containers)
1025    pub clips_content: bool,
1026    /// Overflow fade distances (smooth alpha fade at clip edges). Applied when clips_content is true.
1027    pub overflow_fade: OverflowFade,
1028    /// Motion animation configuration (enter/exit animations)
1029    pub motion: Option<MotionAnimation>,
1030    /// Stable ID for motion animation tracking across tree rebuilds
1031    /// Used by overlays where the tree is rebuilt every frame
1032    pub motion_stable_id: Option<String>,
1033    /// Whether the motion animation should replay from the beginning
1034    /// Used with motion_stable_id to force animation restart on content change
1035    pub motion_should_replay: bool,
1036    /// Whether the motion animation should start in suspended state
1037    /// When true, the motion starts with opacity 0 and waits for explicit start
1038    pub motion_is_suspended: bool,
1039    /// Callback to invoke when the motion is laid out and ready
1040    /// Used with suspended animations to start the animation after content is mounted
1041    pub motion_on_ready_callback:
1042        Option<std::sync::Arc<dyn Fn(ElementBounds) + Send + Sync + 'static>>,
1043    /// Whether this is a Stack layer that increments z_layer for proper z-ordering
1044    /// When true, entering this node increments the DrawContext's z_layer
1045    pub is_stack_layer: bool,
1046    /// Cursor style when hovering over this element (None = inherit from parent)
1047    pub cursor: Option<CursorStyle>,
1048    /// Whether this element is transparent to hit-testing (pointer-events: none)
1049    /// When true, this element will not capture clicks/hovers - only its children can.
1050    /// Used by Stack layers to allow clicks to pass through to siblings.
1051    pub pointer_events_none: bool,
1052    /// Whether this element has `position: fixed` behavior.
1053    /// Fixed elements are immune to scroll transforms from ancestor scroll containers.
1054    pub is_fixed: bool,
1055    /// Whether this element has `position: sticky` behavior.
1056    /// Sticky elements clamp their position when scrolling past thresholds.
1057    pub is_sticky: bool,
1058    /// Sticky scroll-lock threshold from top (in pixels).
1059    pub sticky_top: Option<f32>,
1060    /// Sticky scroll-lock threshold from bottom (in pixels).
1061    pub sticky_bottom: Option<f32>,
1062    /// CSS z-index for controlling render order within a layer
1063    pub z_index: i32,
1064    /// Text foreground color override (when set, overrides TextData.color during rendering)
1065    pub text_color: Option<[f32; 4]>,
1066    /// Font size override (when set, overrides TextData.font_size during rendering)
1067    pub font_size: Option<f32>,
1068    /// Text shadow (offset, blur, color)
1069    pub text_shadow: Option<Shadow>,
1070    /// Font weight override
1071    pub font_weight: Option<crate::div::FontWeight>,
1072    /// Text decoration override
1073    pub text_decoration: Option<crate::element_style::TextDecoration>,
1074    /// Line height multiplier override
1075    pub line_height: Option<f32>,
1076    /// Text alignment override
1077    pub text_align: Option<crate::div::TextAlign>,
1078    /// Letter spacing override in pixels
1079    pub letter_spacing: Option<f32>,
1080    /// SVG fill color override
1081    pub fill: Option<[f32; 4]>,
1082    /// SVG stroke color override
1083    pub stroke: Option<[f32; 4]>,
1084    /// SVG stroke width override
1085    pub stroke_width: Option<f32>,
1086    /// SVG stroke-dasharray pattern (alternating dash/gap lengths)
1087    pub stroke_dasharray: Option<Vec<f32>>,
1088    /// SVG stroke-dashoffset in pixels
1089    pub stroke_dashoffset: Option<f32>,
1090    /// SVG path `d` attribute data (for path morphing animations)
1091    pub svg_path_data: Option<String>,
1092    /// Transform origin as percentages [x%, y%] (default 50%, 50% = center)
1093    pub transform_origin: Option<[f32; 2]>,
1094    /// Layer effects applied to this element (blur, drop shadow, glow, color matrix)
1095    /// Effects are applied during layer composition when the element is rendered
1096    pub layer_effects: Vec<LayerEffect>,
1097    // 3D transform properties
1098    /// X-axis rotation in degrees
1099    pub rotate_x: Option<f32>,
1100    /// Y-axis rotation in degrees
1101    pub rotate_y: Option<f32>,
1102    /// Perspective distance in pixels
1103    pub perspective: Option<f32>,
1104    /// 3D shape type (0=none, 1=box, 2=sphere, 3=cylinder, 4=torus, 5=capsule)
1105    pub shape_3d: Option<f32>,
1106    /// 3D extrusion depth in pixels
1107    pub depth: Option<f32>,
1108    /// Light direction [x, y, z]
1109    pub light_direction: Option<[f32; 3]>,
1110    /// Light intensity (0.0 - 1.0+)
1111    pub light_intensity: Option<f32>,
1112    /// Ambient light level (0.0 - 1.0)
1113    pub ambient: Option<f32>,
1114    /// Specular power (higher = tighter highlights)
1115    pub specular: Option<f32>,
1116    /// Z-axis translation in pixels (positive = toward viewer)
1117    pub translate_z: Option<f32>,
1118    /// 3D boolean operation type (0=union, 1=subtract, 2=intersect, 3=smooth-union, 4=smooth-subtract, 5=smooth-intersect)
1119    pub op_3d: Option<f32>,
1120    /// Blend radius for smooth boolean operations (in pixels)
1121    pub blend_3d: Option<f32>,
1122    /// CSS clip-path shape function
1123    pub clip_path: Option<ClipPath>,
1124    /// CSS filter functions (grayscale, invert, sepia, brightness, contrast, saturate, hue-rotate)
1125    pub filter: Option<crate::element_style::CssFilter>,
1126    /// DEPRECATED: Whether the motion should start exiting
1127    ///
1128    /// This field is deprecated. Motion exit is now triggered explicitly via
1129    /// `MotionHandle.exit()` / `query_motion(key).exit()` instead of capturing
1130    /// the is_overlay_closing() flag at build time.
1131    ///
1132    /// The old mechanism was flawed because the flag reset after build_content(),
1133    /// breaking multi-frame exit animations.
1134    #[deprecated(
1135        since = "0.1.0",
1136        note = "Use query_motion(key).exit() to explicitly trigger motion exit"
1137    )]
1138    pub motion_is_exiting: bool,
1139    /// CSS visibility (false = hidden, keeps layout space but doesn't render)
1140    pub visible: bool,
1141    /// CSS object-fit override for images (0=cover, 1=contain, 2=fill, 3=scale-down, 4=none)
1142    pub object_fit: Option<u8>,
1143    /// CSS object-position override for images [x, y] in 0.0-1.0 range
1144    pub object_position: Option<[f32; 2]>,
1145    /// CSS loading strategy override (0=eager, 1=lazy)
1146    pub loading_strategy: Option<u8>,
1147    /// CSS placeholder type override (0=none, 1=color, 2=image, 3=skeleton)
1148    pub placeholder_type: Option<u8>,
1149    /// CSS placeholder color override
1150    pub placeholder_color: Option<[f32; 4]>,
1151    /// CSS placeholder image source override
1152    pub placeholder_image: Option<String>,
1153    /// CSS fade-in duration in milliseconds
1154    pub fade_duration_ms: Option<u32>,
1155    /// Per-SVG-tag style overrides from CSS tag-name selectors (e.g., `path { fill: red; }`)
1156    pub svg_tag_styles: HashMap<String, SvgTagStyle>,
1157    /// CSS mix-blend-mode for this element
1158    pub mix_blend_mode: Option<blinc_core::BlendMode>,
1159    /// Text decoration line color override
1160    pub text_decoration_color: Option<[f32; 4]>,
1161    /// Text decoration line thickness override
1162    pub text_decoration_thickness: Option<f32>,
1163    /// CSS text-overflow behavior
1164    pub text_overflow: Option<crate::element_style::TextOverflow>,
1165    /// CSS white-space behavior
1166    pub white_space: Option<crate::element_style::WhiteSpace>,
1167    /// CSS mask-image (URL or gradient)
1168    pub mask_image: Option<blinc_core::MaskImage>,
1169    /// CSS mask-mode
1170    pub mask_mode: Option<blinc_core::MaskMode>,
1171    /// @flow shader name (references a FlowGraph in the stylesheet)
1172    pub flow: Option<String>,
1173    /// Direct @flow graph (from `flow!` macro), bypasses stylesheet lookup
1174    pub flow_graph: Option<Arc<FlowGraph>>,
1175}
1176
1177impl Default for RenderProps {
1178    #[allow(deprecated)]
1179    fn default() -> Self {
1180        Self {
1181            background: None,
1182            border_radius: CornerRadius::default(),
1183            border_radius_explicit: false,
1184            corner_shape: CornerShape::default(),
1185            border_color: None,
1186            border_width: 0.0,
1187            border_sides: BorderSides::default(),
1188            outline_color: None,
1189            outline_width: 0.0,
1190            outline_offset: 0.0,
1191            layer: RenderLayer::default(),
1192            material: None,
1193            node_id: None,
1194            shadow: None,
1195            transform: None,
1196            opacity: 1.0,
1197            clips_content: false,
1198            overflow_fade: OverflowFade::default(),
1199            motion: None,
1200            motion_stable_id: None,
1201            motion_should_replay: false,
1202            motion_is_suspended: false,
1203            motion_on_ready_callback: None,
1204            is_stack_layer: false,
1205            cursor: None,
1206            pointer_events_none: false,
1207            is_fixed: false,
1208            is_sticky: false,
1209            sticky_top: None,
1210            sticky_bottom: None,
1211            layer_effects: Vec::new(),
1212            rotate_x: None,
1213            rotate_y: None,
1214            perspective: None,
1215            shape_3d: None,
1216            depth: None,
1217            light_direction: None,
1218            light_intensity: None,
1219            ambient: None,
1220            specular: None,
1221            translate_z: None,
1222            op_3d: None,
1223            blend_3d: None,
1224            clip_path: None,
1225            filter: None,
1226            motion_is_exiting: false,
1227            z_index: 0,
1228            text_color: None,
1229            font_size: None,
1230            text_shadow: None,
1231            font_weight: None,
1232            text_decoration: None,
1233            line_height: None,
1234            text_align: None,
1235            letter_spacing: None,
1236            fill: None,
1237            stroke: None,
1238            stroke_width: None,
1239            stroke_dasharray: None,
1240            stroke_dashoffset: None,
1241            svg_path_data: None,
1242            transform_origin: None,
1243            visible: true,
1244            object_fit: None,
1245            object_position: None,
1246            loading_strategy: None,
1247            placeholder_type: None,
1248            placeholder_color: None,
1249            placeholder_image: None,
1250            fade_duration_ms: None,
1251            svg_tag_styles: HashMap::new(),
1252            mix_blend_mode: None,
1253            text_decoration_color: None,
1254            text_decoration_thickness: None,
1255            text_overflow: None,
1256            white_space: None,
1257            mask_image: None,
1258            mask_mode: None,
1259            flow: None,
1260            flow_graph: None,
1261        }
1262    }
1263}
1264
1265impl RenderProps {
1266    /// Create new render properties
1267    pub fn new() -> Self {
1268        Self::default()
1269    }
1270
1271    /// Set background brush
1272    pub fn with_background(mut self, brush: impl Into<Brush>) -> Self {
1273        self.background = Some(brush.into());
1274        self
1275    }
1276
1277    /// Set background color
1278    pub fn with_bg_color(mut self, color: Color) -> Self {
1279        self.background = Some(Brush::Solid(color));
1280        self
1281    }
1282
1283    /// Set corner radius
1284    pub fn with_border_radius(mut self, radius: CornerRadius) -> Self {
1285        self.border_radius = radius;
1286        self
1287    }
1288
1289    /// Set uniform corner radius
1290    pub fn with_rounded(mut self, radius: f32) -> Self {
1291        self.border_radius = CornerRadius::uniform(radius);
1292        self
1293    }
1294
1295    /// Set render layer
1296    pub fn with_layer(mut self, layer: RenderLayer) -> Self {
1297        self.layer = layer;
1298        self
1299    }
1300
1301    /// Set material
1302    pub fn with_material(mut self, material: Material) -> Self {
1303        self.material = Some(material);
1304        self
1305    }
1306
1307    /// Set node ID
1308    pub fn with_node_id(mut self, id: LayoutNodeId) -> Self {
1309        self.node_id = Some(id);
1310        self
1311    }
1312
1313    /// Check if this element has a glass material
1314    pub fn is_glass(&self) -> bool {
1315        matches!(self.material, Some(Material::Glass(_)))
1316    }
1317
1318    /// Get the glass material if present
1319    pub fn glass_material(&self) -> Option<&GlassMaterial> {
1320        match &self.material {
1321            Some(Material::Glass(glass)) => Some(glass),
1322            _ => None,
1323        }
1324    }
1325
1326    /// Set drop shadow
1327    pub fn with_shadow(mut self, shadow: Shadow) -> Self {
1328        self.shadow = Some(shadow);
1329        self
1330    }
1331
1332    /// Set transform
1333    pub fn with_transform(mut self, transform: Transform) -> Self {
1334        self.transform = Some(transform);
1335        self
1336    }
1337
1338    /// Set opacity (0.0 = transparent, 1.0 = opaque)
1339    pub fn with_opacity(mut self, opacity: f32) -> Self {
1340        self.opacity = opacity.clamp(0.0, 1.0);
1341        self
1342    }
1343
1344    /// Set whether this element clips its children
1345    pub fn with_clips_content(mut self, clips: bool) -> Self {
1346        self.clips_content = clips;
1347        self
1348    }
1349
1350    /// Set cursor style for hover
1351    pub fn with_cursor(mut self, cursor: CursorStyle) -> Self {
1352        self.cursor = Some(cursor);
1353        self
1354    }
1355
1356    /// Merge changes from another RenderProps, only taking non-default values
1357    ///
1358    /// This allows stateful elements to overlay state-specific changes
1359    /// on top of base properties. Only properties that were explicitly
1360    /// set in `other` will override `self`.
1361    pub fn merge_from(&mut self, other: &RenderProps) {
1362        // Override background if set
1363        if other.background.is_some() {
1364            self.background = other.background.clone();
1365        }
1366        // Override border_radius if explicitly set or non-zero
1367        if other.border_radius_explicit || other.border_radius != CornerRadius::default() {
1368            self.border_radius = other.border_radius;
1369            self.border_radius_explicit = other.border_radius_explicit;
1370        }
1371        // Override border_color if set
1372        if other.border_color.is_some() {
1373            self.border_color = other.border_color;
1374        }
1375        // Override border_width if non-zero
1376        if other.border_width > 0.0 {
1377            self.border_width = other.border_width;
1378        }
1379        // Override layer if non-default
1380        if other.layer != RenderLayer::default() {
1381            self.layer = other.layer;
1382        }
1383        // Override material if set
1384        if other.material.is_some() {
1385            self.material = other.material.clone();
1386        }
1387        // node_id is not merged - keep the original
1388        // Override shadow if set
1389        if other.shadow.is_some() {
1390            self.shadow = other.shadow;
1391        }
1392        // Override transform if set
1393        if other.transform.is_some() {
1394            self.transform = other.transform.clone();
1395        }
1396        // Override opacity if non-default
1397        if (other.opacity - 1.0).abs() > f32::EPSILON {
1398            self.opacity = other.opacity;
1399        }
1400        // Override clips_content if true
1401        if other.clips_content {
1402            self.clips_content = true;
1403        }
1404        // Override motion if set
1405        if other.motion.is_some() {
1406            self.motion = other.motion.clone();
1407        }
1408    }
1409}
1410
1411// ============================================================================
1412// Dynamic Render Props (Value References)
1413// ============================================================================
1414
1415/// Dynamic render props that can hold references to reactive values
1416///
1417/// Unlike `RenderProps` which stores resolved values directly, `DynRenderProps`
1418/// stores references (signal IDs, spring IDs) that are resolved at render time.
1419/// This enables visual property changes without tree rebuilds.
1420///
1421/// # Architecture
1422///
1423/// ```text
1424/// Build Time:                       Render Time:
1425/// ┌────────────┐                   ┌────────────────┐
1426/// │ ElementBuilder                │ ValueContext   │
1427/// │ .opacity(0.5)  ────────────►  │ .reactive      │
1428/// │ .bg_signal(id) ────────────►  │ .animations    │
1429/// └────────────┘                   └────────────────┘
1430///       │                                │
1431///       ▼                                ▼
1432/// ┌────────────┐                   ┌────────────────┐
1433/// │ DynRenderProps               │ Resolved Values │
1434/// │ opacity: DynFloat::Static    │ opacity: 0.5    │
1435/// │ background: DynValue::Signal │ background: Red │
1436/// └────────────┘                   └────────────────┘
1437/// ```
1438#[derive(Clone)]
1439pub struct DynRenderProps {
1440    /// Background fill (can be static or signal-driven)
1441    pub background: Option<DynValue<Brush>>,
1442    /// Corner radius (typically static)
1443    pub border_radius: CornerRadius,
1444    /// Corner shape (superellipse n parameter per corner)
1445    pub corner_shape: CornerShape,
1446    /// Which layer this element renders in
1447    pub layer: RenderLayer,
1448    /// Material applied to this element
1449    pub material: Option<Material>,
1450    /// Node ID for looking up children
1451    pub node_id: Option<LayoutNodeId>,
1452    /// Drop shadow (typically static)
1453    pub shadow: Option<Shadow>,
1454    /// Transform (can be animated)
1455    pub transform: Option<Transform>,
1456    /// Opacity (can be static, signal, or spring animated)
1457    pub opacity: DynFloat,
1458    /// Whether this element clips its children
1459    pub clips_content: bool,
1460    /// Overflow fade distances
1461    pub overflow_fade: OverflowFade,
1462}
1463
1464impl Default for DynRenderProps {
1465    fn default() -> Self {
1466        Self {
1467            background: None,
1468            border_radius: CornerRadius::default(),
1469            corner_shape: CornerShape::default(),
1470            layer: RenderLayer::default(),
1471            material: None,
1472            node_id: None,
1473            shadow: None,
1474            transform: None,
1475            opacity: DynFloat::Static(1.0),
1476            clips_content: false,
1477            overflow_fade: OverflowFade::default(),
1478        }
1479    }
1480}
1481
1482impl DynRenderProps {
1483    /// Create new dynamic render properties
1484    pub fn new() -> Self {
1485        Self::default()
1486    }
1487
1488    /// Resolve all dynamic values using the provided context
1489    ///
1490    /// This is called at render time to get the actual values to draw with.
1491    pub fn resolve(&self, ctx: &ValueContext) -> ResolvedRenderProps {
1492        ResolvedRenderProps {
1493            background: self.background.as_ref().map(|v| v.get(ctx)),
1494            border_radius: self.border_radius,
1495            corner_shape: self.corner_shape,
1496            layer: self.layer,
1497            material: self.material.clone(),
1498            node_id: self.node_id,
1499            shadow: self.shadow,
1500            transform: self.transform.clone(),
1501            opacity: self.opacity.get(ctx),
1502            clips_content: self.clips_content,
1503            overflow_fade: self.overflow_fade,
1504        }
1505    }
1506
1507    /// Convert from static RenderProps
1508    pub fn from_static(props: RenderProps) -> Self {
1509        Self {
1510            background: props.background.map(DynValue::Static),
1511            border_radius: props.border_radius,
1512            corner_shape: props.corner_shape,
1513            layer: props.layer,
1514            material: props.material,
1515            node_id: props.node_id,
1516            shadow: props.shadow,
1517            transform: props.transform,
1518            opacity: DynFloat::Static(props.opacity),
1519            clips_content: props.clips_content,
1520            overflow_fade: props.overflow_fade,
1521        }
1522    }
1523
1524    /// Set static background
1525    pub fn with_background(mut self, brush: impl Into<Brush>) -> Self {
1526        self.background = Some(DynValue::Static(brush.into()));
1527        self
1528    }
1529
1530    /// Set background from a signal reference
1531    pub fn with_background_signal(mut self, signal_id: u64, default: Brush) -> Self {
1532        self.background = Some(DynValue::Signal {
1533            id: signal_id,
1534            default,
1535        });
1536        self
1537    }
1538
1539    /// Set static opacity
1540    pub fn with_opacity(mut self, opacity: f32) -> Self {
1541        self.opacity = DynFloat::Static(opacity.clamp(0.0, 1.0));
1542        self
1543    }
1544
1545    /// Set opacity from a signal reference
1546    pub fn with_opacity_signal(mut self, signal_id: u64, default: f32) -> Self {
1547        self.opacity = DynFloat::Signal {
1548            id: signal_id,
1549            default,
1550        };
1551        self
1552    }
1553
1554    /// Set opacity from a spring animation
1555    pub fn with_opacity_spring(mut self, spring_id: u64, generation: u32, default: f32) -> Self {
1556        self.opacity = DynFloat::Spring {
1557            id: spring_id,
1558            generation,
1559            default,
1560        };
1561        self
1562    }
1563}
1564
1565/// Resolved render props with concrete values
1566///
1567/// This is the result of resolving `DynRenderProps` at render time.
1568/// All dynamic references have been replaced with their current values.
1569#[derive(Clone)]
1570pub struct ResolvedRenderProps {
1571    /// Background fill (resolved)
1572    pub background: Option<Brush>,
1573    /// Corner radius
1574    pub border_radius: CornerRadius,
1575    /// Corner shape (superellipse n parameter per corner)
1576    pub corner_shape: CornerShape,
1577    /// Render layer
1578    pub layer: RenderLayer,
1579    /// Material
1580    pub material: Option<Material>,
1581    /// Node ID
1582    pub node_id: Option<LayoutNodeId>,
1583    /// Drop shadow
1584    pub shadow: Option<Shadow>,
1585    /// Transform
1586    pub transform: Option<Transform>,
1587    /// Opacity (resolved)
1588    pub opacity: f32,
1589    /// Whether this element clips its children
1590    pub clips_content: bool,
1591    /// Overflow fade distances
1592    pub overflow_fade: OverflowFade,
1593}
1594
1595impl Default for ResolvedRenderProps {
1596    fn default() -> Self {
1597        Self {
1598            background: None,
1599            border_radius: CornerRadius::default(),
1600            corner_shape: CornerShape::default(),
1601            layer: RenderLayer::default(),
1602            material: None,
1603            node_id: None,
1604            shadow: None,
1605            transform: None,
1606            opacity: 1.0,
1607            clips_content: false,
1608            overflow_fade: OverflowFade::default(),
1609        }
1610    }
1611}