use crate::animation_builder::{CABasicAnimationBuilder, KeyPath};
use crate::color::Color;
use objc2::rc::Retained;
use objc2_core_foundation::{CFRetained, CFString, CGFloat, CGPoint, CGRect, CGSize};
use objc2_core_graphics::CGColor;
use objc2_core_text::CTFont;
use objc2_foundation::NSString;
use objc2_quartz_core::{
kCAAlignmentCenter, kCAAlignmentJustified, kCAAlignmentLeft, kCAAlignmentNatural,
kCAAlignmentRight, kCATruncationEnd, kCATruncationMiddle, kCATruncationNone,
kCATruncationStart, CABasicAnimation, CATextLayer, CATransform3D,
};
struct PendingAnimation {
name: String,
animation: Retained<CABasicAnimation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextAlign {
#[default]
Natural,
Left,
Right,
Center,
Justified,
}
impl TextAlign {
fn to_ca_alignment(self) -> &'static NSString {
unsafe {
match self {
TextAlign::Natural => kCAAlignmentNatural,
TextAlign::Left => kCAAlignmentLeft,
TextAlign::Right => kCAAlignmentRight,
TextAlign::Center => kCAAlignmentCenter,
TextAlign::Justified => kCAAlignmentJustified,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Truncation {
#[default]
None,
Start,
End,
Middle,
}
impl Truncation {
fn to_ca_truncation(self) -> &'static NSString {
unsafe {
match self {
Truncation::None => kCATruncationNone,
Truncation::Start => kCATruncationStart,
Truncation::End => kCATruncationEnd,
Truncation::Middle => kCATruncationMiddle,
}
}
}
}
#[derive(Default)]
pub struct CATextLayerBuilder {
text: Option<String>,
font: Option<CFRetained<CTFont>>,
font_name: Option<String>,
font_size: Option<CGFloat>,
foreground_color: Option<CFRetained<CGColor>>,
alignment: Option<TextAlign>,
truncation: Option<Truncation>,
wrapped: Option<bool>,
bounds: Option<CGRect>,
position: Option<CGPoint>,
transform: Option<CATransform3D>,
hidden: Option<bool>,
opacity: Option<f32>,
shadow_color: Option<CFRetained<CGColor>>,
shadow_offset: Option<(f64, f64)>,
shadow_radius: Option<f64>,
shadow_opacity: Option<f32>,
scale: Option<f64>,
rotation: Option<f64>,
translation: Option<(f64, f64)>,
animations: Vec<PendingAnimation>,
}
impl CATextLayerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn font(mut self, font: CFRetained<CTFont>) -> Self {
self.font = Some(font);
self
}
pub fn font_name(mut self, name: impl Into<String>) -> Self {
self.font_name = Some(name.into());
self
}
pub fn font_size(mut self, size: CGFloat) -> Self {
self.font_size = Some(size);
self
}
pub fn foreground_color(mut self, color: impl Into<CFRetained<CGColor>>) -> Self {
self.foreground_color = Some(color.into());
self
}
pub fn foreground_rgba(mut self, r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) -> Self {
self.foreground_color = Some(Color::rgba(r, g, b, a).into());
self
}
pub fn alignment(mut self, alignment: TextAlign) -> Self {
self.alignment = Some(alignment);
self
}
pub fn truncation(mut self, truncation: Truncation) -> Self {
self.truncation = Some(truncation);
self
}
pub fn wrapped(mut self, wrapped: bool) -> Self {
self.wrapped = Some(wrapped);
self
}
pub fn bounds(mut self, bounds: CGRect) -> Self {
self.bounds = Some(bounds);
self
}
pub fn size(mut self, width: CGFloat, height: CGFloat) -> Self {
self.bounds = Some(CGRect::new(CGPoint::ZERO, CGSize::new(width, height)));
self
}
pub fn position(mut self, position: CGPoint) -> Self {
self.position = Some(position);
self
}
pub fn transform(mut self, transform: CATransform3D) -> Self {
self.transform = Some(transform);
self
}
pub fn hidden(mut self, hidden: bool) -> Self {
self.hidden = Some(hidden);
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = Some(opacity);
self
}
pub fn shadow_color(mut self, color: impl Into<CFRetained<CGColor>>) -> Self {
self.shadow_color = Some(color.into());
self
}
pub fn shadow_offset(mut self, dx: f64, dy: f64) -> Self {
self.shadow_offset = Some((dx, dy));
self
}
pub fn shadow_radius(mut self, radius: f64) -> Self {
self.shadow_radius = Some(radius);
self
}
pub fn shadow_opacity(mut self, opacity: f32) -> Self {
self.shadow_opacity = Some(opacity);
self
}
pub fn scale(mut self, scale: f64) -> Self {
self.scale = Some(scale);
self
}
pub fn rotation(mut self, radians: f64) -> Self {
self.rotation = Some(radians);
self
}
pub fn translate(mut self, dx: f64, dy: f64) -> Self {
self.translation = Some((dx, dy));
self
}
pub fn animate<F>(mut self, name: impl Into<String>, key_path: KeyPath, configure: F) -> Self
where
F: FnOnce(CABasicAnimationBuilder) -> CABasicAnimationBuilder,
{
let builder = CABasicAnimationBuilder::new(key_path);
let animation = configure(builder).build();
self.animations.push(PendingAnimation {
name: name.into(),
animation,
});
self
}
pub fn build(self) -> Retained<CATextLayer> {
let layer = CATextLayer::new();
if let Some(ref text) = self.text {
let ns_string = NSString::from_str(text);
unsafe {
layer.setString(Some(&ns_string));
}
}
if let Some(ref font) = self.font {
unsafe {
layer.setFont(Some(&**font));
}
} else if let Some(ref font_name) = self.font_name {
let cf_name = CFString::from_str(font_name);
let size = self.font_size.unwrap_or(12.0);
let font = unsafe { CTFont::with_name(&cf_name, size, std::ptr::null()) };
unsafe {
layer.setFont(Some(&*font));
}
}
if let Some(size) = self.font_size {
layer.setFontSize(size);
}
if let Some(ref color) = self.foreground_color {
layer.setForegroundColor(Some(&**color));
}
if let Some(alignment) = self.alignment {
layer.setAlignmentMode(alignment.to_ca_alignment());
}
if let Some(truncation) = self.truncation {
layer.setTruncationMode(truncation.to_ca_truncation());
}
if let Some(wrapped) = self.wrapped {
layer.setWrapped(wrapped);
}
if let Some(bounds) = self.bounds {
layer.setBounds(bounds);
}
if let Some(position) = self.position {
layer.setPosition(position);
}
if let Some(transform) = self.transform {
layer.setTransform(transform);
} else if self.scale.is_some() || self.rotation.is_some() || self.translation.is_some() {
let mut transform = CATransform3D::new_scale(1.0, 1.0, 1.0);
if let Some(s) = self.scale {
transform = CATransform3D::new_scale(s, s, 1.0);
}
if let Some(r) = self.rotation {
let rotation_transform = CATransform3D::new_rotation(r, 0.0, 0.0, 1.0);
transform = transform.concat(rotation_transform);
}
if let Some((dx, dy)) = self.translation {
let translation_transform = CATransform3D::new_translation(dx, dy, 0.0);
transform = transform.concat(translation_transform);
}
layer.setTransform(transform);
}
if let Some(hidden) = self.hidden {
layer.setHidden(hidden);
}
if let Some(opacity) = self.opacity {
layer.setOpacity(opacity);
}
if let Some(ref color) = self.shadow_color {
layer.setShadowColor(Some(&**color));
}
if let Some((dx, dy)) = self.shadow_offset {
layer.setShadowOffset(CGSize::new(dx, dy));
}
if let Some(radius) = self.shadow_radius {
layer.setShadowRadius(radius);
}
if let Some(opacity) = self.shadow_opacity {
layer.setShadowOpacity(opacity);
}
for pending in self.animations {
let key = NSString::from_str(&pending.name);
layer.addAnimation_forKey(&pending.animation, Some(&key));
}
layer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_align_default() {
assert_eq!(TextAlign::default(), TextAlign::Natural);
}
#[test]
fn test_truncation_default() {
assert_eq!(Truncation::default(), Truncation::None);
}
#[test]
fn test_builder_default() {
let builder = CATextLayerBuilder::new();
assert!(builder.text.is_none());
assert!(builder.font.is_none());
assert!(builder.font_name.is_none());
assert!(builder.font_size.is_none());
assert!(builder.foreground_color.is_none());
assert!(builder.alignment.is_none());
assert!(builder.truncation.is_none());
assert!(builder.wrapped.is_none());
assert!(builder.bounds.is_none());
assert!(builder.position.is_none());
assert!(builder.opacity.is_none());
assert!(builder.animations.is_empty());
}
#[test]
fn test_builder_chaining() {
let builder = CATextLayerBuilder::new()
.text("Hello")
.font_name("Helvetica")
.font_size(24.0)
.alignment(TextAlign::Center)
.truncation(Truncation::End)
.wrapped(true)
.opacity(0.8);
assert_eq!(builder.text.as_deref(), Some("Hello"));
assert_eq!(builder.font_name.as_deref(), Some("Helvetica"));
assert_eq!(builder.font_size, Some(24.0));
assert_eq!(builder.alignment, Some(TextAlign::Center));
assert_eq!(builder.truncation, Some(Truncation::End));
assert_eq!(builder.wrapped, Some(true));
assert_eq!(builder.opacity, Some(0.8));
}
#[test]
fn test_size_convenience() {
let builder = CATextLayerBuilder::new().size(200.0, 50.0);
let bounds = builder.bounds.unwrap();
assert!((bounds.size.width - 200.0).abs() < f64::EPSILON);
assert!((bounds.size.height - 50.0).abs() < f64::EPSILON);
}
}