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}