core_animation/
text_layer_builder.rs

1//! Builder for `CATextLayer` (text rendering layer).
2
3use crate::animation_builder::{CABasicAnimationBuilder, KeyPath};
4use crate::color::Color;
5use objc2::rc::Retained;
6use objc2_core_foundation::{CFRetained, CFString, CGFloat, CGPoint, CGRect, CGSize};
7use objc2_core_graphics::CGColor;
8use objc2_core_text::CTFont;
9use objc2_foundation::NSString;
10use objc2_quartz_core::{
11    kCAAlignmentCenter, kCAAlignmentJustified, kCAAlignmentLeft, kCAAlignmentNatural,
12    kCAAlignmentRight, kCATruncationEnd, kCATruncationMiddle, kCATruncationNone,
13    kCATruncationStart, CABasicAnimation, CATextLayer, CATransform3D,
14};
15
16/// A pending animation to be applied when the layer is built.
17struct PendingAnimation {
18    name: String,
19    animation: Retained<CABasicAnimation>,
20}
21
22/// Text alignment modes for `CATextLayer`.
23///
24/// These map to Core Animation's text alignment constants.
25///
26/// # Examples
27///
28/// ```ignore
29/// CATextLayerBuilder::new()
30///     .text("Hello, World!")
31///     .alignment(TextAlign::Center)
32///     .build();
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum TextAlign {
36    /// Natural alignment based on the localization setting of the system.
37    /// This is the default alignment.
38    #[default]
39    Natural,
40    /// Left-aligned text.
41    Left,
42    /// Right-aligned text.
43    Right,
44    /// Center-aligned text.
45    Center,
46    /// Justified text (both left and right edges aligned).
47    Justified,
48}
49
50impl TextAlign {
51    /// Returns the Core Animation alignment mode string for this alignment.
52    fn to_ca_alignment(self) -> &'static NSString {
53        // SAFETY: These extern statics are always valid on macOS.
54        unsafe {
55            match self {
56                TextAlign::Natural => kCAAlignmentNatural,
57                TextAlign::Left => kCAAlignmentLeft,
58                TextAlign::Right => kCAAlignmentRight,
59                TextAlign::Center => kCAAlignmentCenter,
60                TextAlign::Justified => kCAAlignmentJustified,
61            }
62        }
63    }
64}
65
66/// Truncation modes for `CATextLayer`.
67///
68/// These control how text is truncated when it doesn't fit in the layer bounds.
69///
70/// # Examples
71///
72/// ```ignore
73/// CATextLayerBuilder::new()
74///     .text("This is a very long text that might be truncated...")
75///     .truncation(Truncation::End)
76///     .build();
77/// ```
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
79pub enum Truncation {
80    /// No truncation. Text may overflow the layer bounds.
81    #[default]
82    None,
83    /// Truncate at the start of the text (e.g., "...long text").
84    Start,
85    /// Truncate at the end of the text (e.g., "Long text...").
86    End,
87    /// Truncate in the middle of the text (e.g., "Long...text").
88    Middle,
89}
90
91impl Truncation {
92    /// Returns the Core Animation truncation mode string for this truncation mode.
93    fn to_ca_truncation(self) -> &'static NSString {
94        // SAFETY: These extern statics are always valid on macOS.
95        unsafe {
96            match self {
97                Truncation::None => kCATruncationNone,
98                Truncation::Start => kCATruncationStart,
99                Truncation::End => kCATruncationEnd,
100                Truncation::Middle => kCATruncationMiddle,
101            }
102        }
103    }
104}
105
106/// Builder for `CATextLayer`.
107///
108/// `CATextLayer` renders text using Core Text. This builder provides an ergonomic
109/// API for configuring text layers with fonts, colors, alignment, and animations.
110///
111/// # Basic Usage
112///
113/// ```ignore
114/// let text = CATextLayerBuilder::new()
115///     .text("Hello, World!")
116///     .font_size(24.0)
117///     .foreground_color(Color::WHITE)
118///     .alignment(TextAlign::Center)
119///     .build();
120/// ```
121///
122/// # With Font Name
123///
124/// ```ignore
125/// let text = CATextLayerBuilder::new()
126///     .text("Monospaced")
127///     .font_name("Menlo")
128///     .font_size(16.0)
129///     .foreground_color(Color::CYAN)
130///     .build();
131/// ```
132///
133/// # With CTFont
134///
135/// For more control over font attributes, you can provide a `CTFont` directly:
136///
137/// ```ignore
138/// let font = unsafe {
139///     CTFont::with_name(&CFString::from_static_str("Helvetica-Bold"), 18.0, std::ptr::null())
140/// };
141///
142/// let text = CATextLayerBuilder::new()
143///     .text("Bold Text")
144///     .font(font)
145///     .foreground_color(Color::ORANGE)
146///     .build();
147/// ```
148///
149/// # With Animations
150///
151/// Animations can be added inline using the `.animate()` method:
152///
153/// ```ignore
154/// let text = CATextLayerBuilder::new()
155///     .text("Pulsing Text")
156///     .font_size(32.0)
157///     .foreground_color(Color::RED)
158///     .animate("pulse", KeyPath::TransformScale, |a| {
159///         a.values(0.9, 1.1)
160///             .duration(500.millis())
161///             .autoreverses()
162///             .repeat(Repeat::Forever)
163///     })
164///     .build();
165/// ```
166///
167/// # Text Wrapping and Truncation
168///
169/// ```ignore
170/// let text = CATextLayerBuilder::new()
171///     .text("This is a long text that will wrap to multiple lines")
172///     .bounds(CGRect::new(CGPoint::ZERO, CGSize::new(200.0, 100.0)))
173///     .wrapped(true)
174///     .truncation(Truncation::End)
175///     .build();
176/// ```
177#[derive(Default)]
178pub struct CATextLayerBuilder {
179    // Text content
180    text: Option<String>,
181
182    // Font properties
183    font: Option<CFRetained<CTFont>>,
184    font_name: Option<String>,
185    font_size: Option<CGFloat>,
186
187    // Appearance
188    foreground_color: Option<CFRetained<CGColor>>,
189    alignment: Option<TextAlign>,
190    truncation: Option<Truncation>,
191    wrapped: Option<bool>,
192
193    // Layer geometry
194    bounds: Option<CGRect>,
195    position: Option<CGPoint>,
196    transform: Option<CATransform3D>,
197
198    // Layer properties
199    hidden: Option<bool>,
200    opacity: Option<f32>,
201
202    // Shadow properties
203    shadow_color: Option<CFRetained<CGColor>>,
204    shadow_offset: Option<(f64, f64)>,
205    shadow_radius: Option<f64>,
206    shadow_opacity: Option<f32>,
207
208    // Simple transform shortcuts
209    scale: Option<f64>,
210    rotation: Option<f64>,
211    translation: Option<(f64, f64)>,
212
213    // Animations
214    animations: Vec<PendingAnimation>,
215}
216
217impl CATextLayerBuilder {
218    /// Creates a new builder with default values.
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    // ========================================================================
224    // Text content
225    // ========================================================================
226
227    /// Sets the text content to display.
228    ///
229    /// # Arguments
230    ///
231    /// * `text` - The string to render
232    ///
233    /// # Examples
234    ///
235    /// ```ignore
236    /// CATextLayerBuilder::new()
237    ///     .text("Hello, World!")
238    ///     .build();
239    /// ```
240    pub fn text(mut self, text: impl Into<String>) -> Self {
241        self.text = Some(text.into());
242        self
243    }
244
245    // ========================================================================
246    // Font properties
247    // ========================================================================
248
249    /// Sets the font using a `CTFont` object.
250    ///
251    /// This gives you full control over the font, including traits like bold,
252    /// italic, etc.
253    ///
254    /// # Arguments
255    ///
256    /// * `font` - A Core Text font object
257    ///
258    /// # Examples
259    ///
260    /// ```ignore
261    /// let font = unsafe {
262    ///     CTFont::with_name(
263    ///         &CFString::from_static_str("Helvetica-Bold"),
264    ///         18.0,
265    ///         std::ptr::null()
266    ///     )
267    /// };
268    ///
269    /// CATextLayerBuilder::new()
270    ///     .text("Bold Text")
271    ///     .font(font)
272    ///     .build();
273    /// ```
274    ///
275    /// # Notes
276    ///
277    /// When `.font()` is set, it takes precedence over `.font_name()`.
278    /// The font size from the `CTFont` will be used unless `.font_size()` is
279    /// also called.
280    pub fn font(mut self, font: CFRetained<CTFont>) -> Self {
281        self.font = Some(font);
282        self
283    }
284
285    /// Sets the font by name (PostScript name preferred).
286    ///
287    /// Common font names include:
288    /// - "Helvetica", "Helvetica-Bold", "Helvetica-Oblique"
289    /// - "Menlo", "Menlo-Bold" (monospaced)
290    /// - "SF Pro", "SF Pro Display" (system fonts on modern macOS)
291    /// - "Times New Roman"
292    ///
293    /// # Arguments
294    ///
295    /// * `name` - The font name (PostScript name preferred)
296    ///
297    /// # Examples
298    ///
299    /// ```ignore
300    /// CATextLayerBuilder::new()
301    ///     .text("Monospaced")
302    ///     .font_name("Menlo")
303    ///     .font_size(14.0)
304    ///     .build();
305    /// ```
306    ///
307    /// # Notes
308    ///
309    /// If `.font()` is also set, it takes precedence over `.font_name()`.
310    /// Use `.font_size()` to set the size when using `.font_name()`.
311    pub fn font_name(mut self, name: impl Into<String>) -> Self {
312        self.font_name = Some(name.into());
313        self
314    }
315
316    /// Sets the font size in points.
317    ///
318    /// # Arguments
319    ///
320    /// * `size` - Font size in points (e.g., 12.0, 16.0, 24.0)
321    ///
322    /// # Examples
323    ///
324    /// ```ignore
325    /// CATextLayerBuilder::new()
326    ///     .text("Large Text")
327    ///     .font_size(48.0)
328    ///     .build();
329    /// ```
330    ///
331    /// # Notes
332    ///
333    /// If `.font()` is set, the font size from the `CTFont` is used unless
334    /// `.font_size()` is explicitly called to override it.
335    pub fn font_size(mut self, size: CGFloat) -> Self {
336        self.font_size = Some(size);
337        self
338    }
339
340    // ========================================================================
341    // Appearance
342    // ========================================================================
343
344    /// Sets the text foreground color.
345    ///
346    /// Accepts any type that implements `Into<CFRetained<CGColor>>`, including:
347    /// - `Color::RED`, `Color::rgb(1.0, 0.0, 0.0)`
348    /// - `CFRetained<CGColor>` directly
349    ///
350    /// # Examples
351    ///
352    /// ```ignore
353    /// CATextLayerBuilder::new()
354    ///     .text("Red Text")
355    ///     .foreground_color(Color::RED)
356    ///     .build();
357    /// ```
358    pub fn foreground_color(mut self, color: impl Into<CFRetained<CGColor>>) -> Self {
359        self.foreground_color = Some(color.into());
360        self
361    }
362
363    /// Sets the text foreground color from RGBA values (0.0-1.0).
364    ///
365    /// # Examples
366    ///
367    /// ```ignore
368    /// CATextLayerBuilder::new()
369    ///     .text("Orange Text")
370    ///     .foreground_rgba(1.0, 0.5, 0.0, 1.0)
371    ///     .build();
372    /// ```
373    pub fn foreground_rgba(mut self, r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) -> Self {
374        self.foreground_color = Some(Color::rgba(r, g, b, a).into());
375        self
376    }
377
378    /// Sets the text alignment.
379    ///
380    /// # Arguments
381    ///
382    /// * `alignment` - The text alignment mode
383    ///
384    /// # Examples
385    ///
386    /// ```ignore
387    /// CATextLayerBuilder::new()
388    ///     .text("Centered")
389    ///     .alignment(TextAlign::Center)
390    ///     .build();
391    /// ```
392    pub fn alignment(mut self, alignment: TextAlign) -> Self {
393        self.alignment = Some(alignment);
394        self
395    }
396
397    /// Sets the truncation mode for text that doesn't fit.
398    ///
399    /// # Arguments
400    ///
401    /// * `truncation` - The truncation mode
402    ///
403    /// # Examples
404    ///
405    /// ```ignore
406    /// CATextLayerBuilder::new()
407    ///     .text("This text is too long to fit...")
408    ///     .truncation(Truncation::End)
409    ///     .build();
410    /// ```
411    pub fn truncation(mut self, truncation: Truncation) -> Self {
412        self.truncation = Some(truncation);
413        self
414    }
415
416    /// Sets whether text should wrap to multiple lines.
417    ///
418    /// When `true`, text wraps at word boundaries when it exceeds the layer width.
419    /// When `false` (default), text remains on a single line.
420    ///
421    /// # Arguments
422    ///
423    /// * `wrapped` - Whether to enable text wrapping
424    ///
425    /// # Examples
426    ///
427    /// ```ignore
428    /// CATextLayerBuilder::new()
429    ///     .text("This is a long text that will wrap")
430    ///     .bounds(CGRect::new(CGPoint::ZERO, CGSize::new(100.0, 200.0)))
431    ///     .wrapped(true)
432    ///     .build();
433    /// ```
434    pub fn wrapped(mut self, wrapped: bool) -> Self {
435        self.wrapped = Some(wrapped);
436        self
437    }
438
439    // ========================================================================
440    // Layer geometry
441    // ========================================================================
442
443    /// Sets the bounds rectangle.
444    ///
445    /// The bounds define the layer's size and the coordinate space for sublayers.
446    /// For text layers, bounds control the area where text is rendered.
447    ///
448    /// # Examples
449    ///
450    /// ```ignore
451    /// CATextLayerBuilder::new()
452    ///     .text("Text in a box")
453    ///     .bounds(CGRect::new(CGPoint::ZERO, CGSize::new(200.0, 50.0)))
454    ///     .build();
455    /// ```
456    pub fn bounds(mut self, bounds: CGRect) -> Self {
457        self.bounds = Some(bounds);
458        self
459    }
460
461    /// Sets the bounds from width and height (origin at ZERO).
462    ///
463    /// Convenience method equivalent to:
464    /// ```ignore
465    /// .bounds(CGRect::new(CGPoint::ZERO, CGSize::new(width, height)))
466    /// ```
467    ///
468    /// # Examples
469    ///
470    /// ```ignore
471    /// CATextLayerBuilder::new()
472    ///     .text("Sized text area")
473    ///     .size(200.0, 50.0)
474    ///     .build();
475    /// ```
476    pub fn size(mut self, width: CGFloat, height: CGFloat) -> Self {
477        self.bounds = Some(CGRect::new(CGPoint::ZERO, CGSize::new(width, height)));
478        self
479    }
480
481    /// Sets the position in superlayer coordinates.
482    ///
483    /// The position is where the layer's anchor point is placed in the superlayer.
484    /// By default, the anchor point is at the center of the layer (0.5, 0.5).
485    ///
486    /// # Examples
487    ///
488    /// ```ignore
489    /// CATextLayerBuilder::new()
490    ///     .text("Positioned text")
491    ///     .position(CGPoint::new(100.0, 100.0))
492    ///     .build();
493    /// ```
494    pub fn position(mut self, position: CGPoint) -> Self {
495        self.position = Some(position);
496        self
497    }
498
499    /// Sets the 3D transform.
500    ///
501    /// # Examples
502    ///
503    /// ```ignore
504    /// let rotate = CATransform3D::new_rotation(0.1, 0.0, 0.0, 1.0);
505    /// CATextLayerBuilder::new()
506    ///     .text("Rotated")
507    ///     .transform(rotate)
508    ///     .build();
509    /// ```
510    pub fn transform(mut self, transform: CATransform3D) -> Self {
511        self.transform = Some(transform);
512        self
513    }
514
515    // ========================================================================
516    // Layer properties
517    // ========================================================================
518
519    /// Sets whether the layer is hidden.
520    pub fn hidden(mut self, hidden: bool) -> Self {
521        self.hidden = Some(hidden);
522        self
523    }
524
525    /// Sets the opacity (0.0-1.0).
526    pub fn opacity(mut self, opacity: f32) -> Self {
527        self.opacity = Some(opacity);
528        self
529    }
530
531    // ========================================================================
532    // Shadow properties
533    // ========================================================================
534
535    /// Sets the shadow color.
536    ///
537    /// # Examples
538    ///
539    /// ```ignore
540    /// CATextLayerBuilder::new()
541    ///     .text("Shadowed")
542    ///     .shadow_color(Color::BLACK)
543    ///     .shadow_radius(5.0)
544    ///     .shadow_opacity(0.5)
545    ///     .build();
546    /// ```
547    pub fn shadow_color(mut self, color: impl Into<CFRetained<CGColor>>) -> Self {
548        self.shadow_color = Some(color.into());
549        self
550    }
551
552    /// Sets the shadow offset (dx, dy).
553    ///
554    /// Positive `dx` moves the shadow right, positive `dy` moves it down.
555    pub fn shadow_offset(mut self, dx: f64, dy: f64) -> Self {
556        self.shadow_offset = Some((dx, dy));
557        self
558    }
559
560    /// Sets the shadow blur radius.
561    ///
562    /// Larger values create a softer, more diffuse shadow.
563    pub fn shadow_radius(mut self, radius: f64) -> Self {
564        self.shadow_radius = Some(radius);
565        self
566    }
567
568    /// Sets the shadow opacity (0.0 to 1.0).
569    pub fn shadow_opacity(mut self, opacity: f32) -> Self {
570        self.shadow_opacity = Some(opacity);
571        self
572    }
573
574    // ========================================================================
575    // Simple transform shortcuts
576    // ========================================================================
577
578    /// Sets a uniform scale transform.
579    ///
580    /// # Notes
581    ///
582    /// When multiple transform shortcuts are set, they are composed in order:
583    /// scale -> rotation -> translation.
584    ///
585    /// If you also call `.transform()`, the explicit transform takes
586    /// precedence and `scale`/`rotation`/`translate` are ignored.
587    pub fn scale(mut self, scale: f64) -> Self {
588        self.scale = Some(scale);
589        self
590    }
591
592    /// Sets a z-axis rotation transform (in radians).
593    ///
594    /// For degrees, use: `.rotation(45.0_f64.to_radians())`
595    ///
596    /// # Notes
597    ///
598    /// When multiple transform shortcuts are set, they are composed in order:
599    /// scale -> rotation -> translation.
600    pub fn rotation(mut self, radians: f64) -> Self {
601        self.rotation = Some(radians);
602        self
603    }
604
605    /// Sets a translation transform (dx, dy).
606    ///
607    /// # Notes
608    ///
609    /// When multiple transform shortcuts are set, they are composed in order:
610    /// scale -> rotation -> translation.
611    pub fn translate(mut self, dx: f64, dy: f64) -> Self {
612        self.translation = Some((dx, dy));
613        self
614    }
615
616    // ========================================================================
617    // Animations
618    // ========================================================================
619
620    /// Adds an animation to be applied when the layer is built.
621    ///
622    /// The animation is configured using a closure that receives a
623    /// [`CABasicAnimationBuilder`] and returns the configured builder.
624    ///
625    /// # Arguments
626    ///
627    /// * `name` - A unique identifier for this animation (used as the animation key)
628    /// * `key_path` - The property to animate (e.g., [`KeyPath::TransformScale`])
629    /// * `configure` - A closure that configures the animation builder
630    ///
631    /// # Examples
632    ///
633    /// ```ignore
634    /// // Pulsing text
635    /// CATextLayerBuilder::new()
636    ///     .text("Pulsing")
637    ///     .font_size(24.0)
638    ///     .foreground_color(Color::CYAN)
639    ///     .animate("pulse", KeyPath::TransformScale, |a| {
640    ///         a.values(0.9, 1.1)
641    ///             .duration(500.millis())
642    ///             .autoreverses()
643    ///             .repeat(Repeat::Forever)
644    ///     })
645    ///     .build();
646    ///
647    /// // Fading text
648    /// CATextLayerBuilder::new()
649    ///     .text("Fading")
650    ///     .foreground_color(Color::WHITE)
651    ///     .animate("fade", KeyPath::Opacity, |a| {
652    ///         a.values(1.0, 0.3)
653    ///             .duration(1.seconds())
654    ///             .autoreverses()
655    ///             .repeat(Repeat::Forever)
656    ///     })
657    ///     .build();
658    /// ```
659    pub fn animate<F>(mut self, name: impl Into<String>, key_path: KeyPath, configure: F) -> Self
660    where
661        F: FnOnce(CABasicAnimationBuilder) -> CABasicAnimationBuilder,
662    {
663        let builder = CABasicAnimationBuilder::new(key_path);
664        let animation = configure(builder).build();
665        self.animations.push(PendingAnimation {
666            name: name.into(),
667            animation,
668        });
669        self
670    }
671
672    // ========================================================================
673    // Build
674    // ========================================================================
675
676    /// Builds and returns the configured `CATextLayer`.
677    ///
678    /// All pending animations added via `.animate()` are applied to the layer.
679    pub fn build(self) -> Retained<CATextLayer> {
680        let layer = CATextLayer::new();
681
682        // Set text content
683        if let Some(ref text) = self.text {
684            let ns_string = NSString::from_str(text);
685            // SAFETY: NSString is a valid object type for the string property
686            unsafe {
687                layer.setString(Some(&ns_string));
688            }
689        }
690
691        // Set font - CTFont takes precedence over font_name
692        if let Some(ref font) = self.font {
693            // SAFETY: CTFont is toll-free bridged with NSFont and is valid for setFont
694            unsafe {
695                layer.setFont(Some(&**font));
696            }
697        } else if let Some(ref font_name) = self.font_name {
698            // Create CTFont from name and set it
699            let cf_name = CFString::from_str(font_name);
700            let size = self.font_size.unwrap_or(12.0);
701            // SAFETY: null matrix is valid and means identity transform
702            let font = unsafe { CTFont::with_name(&cf_name, size, std::ptr::null()) };
703            // SAFETY: CTFont is valid for setFont
704            unsafe {
705                layer.setFont(Some(&*font));
706            }
707        }
708
709        // Set font size (overrides font's size if both are set)
710        if let Some(size) = self.font_size {
711            layer.setFontSize(size);
712        }
713
714        // Set foreground color
715        if let Some(ref color) = self.foreground_color {
716            layer.setForegroundColor(Some(&**color));
717        }
718
719        // Set alignment
720        if let Some(alignment) = self.alignment {
721            layer.setAlignmentMode(alignment.to_ca_alignment());
722        }
723
724        // Set truncation mode
725        if let Some(truncation) = self.truncation {
726            layer.setTruncationMode(truncation.to_ca_truncation());
727        }
728
729        // Set wrapping
730        if let Some(wrapped) = self.wrapped {
731            layer.setWrapped(wrapped);
732        }
733
734        // Set geometry
735        if let Some(bounds) = self.bounds {
736            layer.setBounds(bounds);
737        }
738        if let Some(position) = self.position {
739            layer.setPosition(position);
740        }
741
742        // Transform handling: explicit transform takes precedence over shortcuts
743        if let Some(transform) = self.transform {
744            layer.setTransform(transform);
745        } else if self.scale.is_some() || self.rotation.is_some() || self.translation.is_some() {
746            // Compose transforms in order: scale -> rotation -> translation
747            let mut transform = CATransform3D::new_scale(1.0, 1.0, 1.0); // identity
748
749            if let Some(s) = self.scale {
750                transform = CATransform3D::new_scale(s, s, 1.0);
751            }
752
753            if let Some(r) = self.rotation {
754                let rotation_transform = CATransform3D::new_rotation(r, 0.0, 0.0, 1.0);
755                transform = transform.concat(rotation_transform);
756            }
757
758            if let Some((dx, dy)) = self.translation {
759                let translation_transform = CATransform3D::new_translation(dx, dy, 0.0);
760                transform = transform.concat(translation_transform);
761            }
762
763            layer.setTransform(transform);
764        }
765
766        // Set layer properties
767        if let Some(hidden) = self.hidden {
768            layer.setHidden(hidden);
769        }
770        if let Some(opacity) = self.opacity {
771            layer.setOpacity(opacity);
772        }
773
774        // Apply shadow properties
775        if let Some(ref color) = self.shadow_color {
776            layer.setShadowColor(Some(&**color));
777        }
778        if let Some((dx, dy)) = self.shadow_offset {
779            layer.setShadowOffset(CGSize::new(dx, dy));
780        }
781        if let Some(radius) = self.shadow_radius {
782            layer.setShadowRadius(radius);
783        }
784        if let Some(opacity) = self.shadow_opacity {
785            layer.setShadowOpacity(opacity);
786        }
787
788        // Apply all pending animations
789        for pending in self.animations {
790            let key = NSString::from_str(&pending.name);
791            layer.addAnimation_forKey(&pending.animation, Some(&key));
792        }
793
794        layer
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801
802    #[test]
803    fn test_text_align_default() {
804        assert_eq!(TextAlign::default(), TextAlign::Natural);
805    }
806
807    #[test]
808    fn test_truncation_default() {
809        assert_eq!(Truncation::default(), Truncation::None);
810    }
811
812    #[test]
813    fn test_builder_default() {
814        let builder = CATextLayerBuilder::new();
815        assert!(builder.text.is_none());
816        assert!(builder.font.is_none());
817        assert!(builder.font_name.is_none());
818        assert!(builder.font_size.is_none());
819        assert!(builder.foreground_color.is_none());
820        assert!(builder.alignment.is_none());
821        assert!(builder.truncation.is_none());
822        assert!(builder.wrapped.is_none());
823        assert!(builder.bounds.is_none());
824        assert!(builder.position.is_none());
825        assert!(builder.opacity.is_none());
826        assert!(builder.animations.is_empty());
827    }
828
829    #[test]
830    fn test_builder_chaining() {
831        let builder = CATextLayerBuilder::new()
832            .text("Hello")
833            .font_name("Helvetica")
834            .font_size(24.0)
835            .alignment(TextAlign::Center)
836            .truncation(Truncation::End)
837            .wrapped(true)
838            .opacity(0.8);
839
840        assert_eq!(builder.text.as_deref(), Some("Hello"));
841        assert_eq!(builder.font_name.as_deref(), Some("Helvetica"));
842        assert_eq!(builder.font_size, Some(24.0));
843        assert_eq!(builder.alignment, Some(TextAlign::Center));
844        assert_eq!(builder.truncation, Some(Truncation::End));
845        assert_eq!(builder.wrapped, Some(true));
846        assert_eq!(builder.opacity, Some(0.8));
847    }
848
849    #[test]
850    fn test_size_convenience() {
851        let builder = CATextLayerBuilder::new().size(200.0, 50.0);
852
853        let bounds = builder.bounds.unwrap();
854        assert!((bounds.size.width - 200.0).abs() < f64::EPSILON);
855        assert!((bounds.size.height - 50.0).abs() < f64::EPSILON);
856    }
857}