cgs 0.1.0

a ui library for macos
use std::fmt;
use std::ptr::{self, NonNull};

use objc2_core_foundation::{CFRetained, CFString, CFType, CGPoint, CGRect, CGSize, Type};
use objc2_core_graphics::{CGContext, CGError};

use super::cg_ok;
use super::private::{
    CGRegionCreateEmptyRegion, CGSNewRegionWithRect, CGSNewRegionWithRectList, G_CONNECTION,
    SLSClearWindowTags, SLSFlushWindowContentRegion, SLSNewWindowWithOpaqueShapeAndContext,
    SLSOrderWindow, SLSReleaseWindow, SLSSetWindowAlpha, SLSSetWindowBackgroundBlurRadius,
    SLSSetWindowBackgroundBlurRadiusStyle, SLSSetWindowLevel, SLSSetWindowOpacity,
    SLSSetWindowProperty, SLSSetWindowResolution, SLSSetWindowShape, SLSSetWindowSubLevel,
    SLSSetWindowTags, SLWindowContextCreate, cid_t,
};

type WindowId = u32;
const TAG_BITSET_LEN: i32 = 64;
const DEFAULT_SUBLEVEL: i32 = 0;
const COMPOSITOR_WINDOW_OPTIONS: i32 = 13 | (1 << 18);

#[repr(transparent)]
pub(super) struct CFRegion(CFRetained<CFType>);

impl CFRegion {
    fn from_rect(rect: &CGRect) -> Result<Self, CGError> {
        let mut region: *mut CFType = ptr::null_mut();
        cg_ok(unsafe { CGSNewRegionWithRect(rect, &mut region) })?;
        let region = NonNull::new(region).ok_or(CGError(1000))?;
        Ok(Self(unsafe { CFRetained::from_raw(region) }))
    }

    pub(super) fn from_rounded_rect(size: CGSize, radius: f64) -> Result<Self, CGError> {
        let radius = radius.max(0.0).min(size.width.max(0.0) / 2.0).min(size.height.max(0.0) / 2.0);
        if radius == 0.0 {
            return Self::from_rect(&CGRect::new(CGPoint::new(0.0, 0.0), size));
        }

        const STEP: f64 = 0.5;
        let mut rects = Vec::with_capacity((radius / STEP).ceil() as usize * 2 + 1);
        let middle_height = (size.height - radius * 2.0).max(0.0);
        if middle_height > 0.0 {
            rects.push(CGRect::new(
                CGPoint::new(0.0, radius),
                CGSize::new(size.width, middle_height),
            ));
        }

        let mut y = 0.0;
        while y < radius {
            let height = STEP.min(radius - y);
            let sample = y + height / 2.0;
            let dy = radius - sample;
            let inset = radius - (radius * radius - dy * dy).max(0.0).sqrt();
            let width = (size.width - inset * 2.0).max(0.0);
            rects.push(CGRect::new(CGPoint::new(inset, y), CGSize::new(width, height)));
            rects.push(CGRect::new(
                CGPoint::new(inset, size.height - y - height),
                CGSize::new(width, height),
            ));
            y += height;
        }

        let mut region = ptr::null_mut();
        cg_ok(unsafe {
            CGSNewRegionWithRectList(rects.as_ptr(), rects.len() as i32, &mut region)
        })?;
        NonNull::new(region)
            .map(|region| Self(unsafe { CFRetained::from_raw(region) }))
            .ok_or(CGError(1000))
    }

    fn empty() -> Result<Self, CGError> {
        let region = NonNull::new(unsafe { CGRegionCreateEmptyRegion() }).ok_or(CGError(1000))?;
        Ok(Self(unsafe { CFRetained::from_raw(region) }))
    }

    #[inline]
    pub(super) fn as_ptr(&self) -> *mut CFType { CFRetained::<CFType>::as_ptr(&self.0).as_ptr() }
}

#[derive(Debug)]
pub enum CgsWindowError {
    Region(CGError),
    Window(CGError),
    Resolution(CGError),
    Alpha(CGError),
    Blur(CGError),
    Level(CGError),
    Shape(CGError),
    Surface(CGError),
    Tags(CGError),
    Release(CGError),
    Property(CGError),
}

impl fmt::Display for CgsWindowError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use CgsWindowError::*;
        match self {
            Region(e) => write!(f, "CGS region error: {:?}", e),
            Window(e) => write!(f, "CGS window create error: {:?}", e),
            Resolution(e) => write!(f, "CGS window resolution error: {:?}", e),
            Alpha(e) => write!(f, "CGS window alpha/opacity error: {:?}", e),
            Blur(e) => write!(f, "CGS window blur error: {:?}", e),
            Level(e) => write!(f, "CGS window level/order error: {:?}", e),
            Shape(e) => write!(f, "CGS window shape error: {:?}", e),
            Surface(e) => write!(f, "CGS window surface error: {:?}", e),
            Tags(e) => write!(f, "CGS window tags error: {:?}", e),
            Release(e) => write!(f, "CGS window release error: {:?}", e),
            Property(e) => write!(f, "CGS window property error: {:?}", e),
        }
    }
}

impl std::error::Error for CgsWindowError {}

#[derive(Debug)]
pub struct CgsWindow {
    id: WindowId,
    connection: cid_t,
    owned: bool,
}

impl CgsWindow {
    pub fn new(frame: CGRect) -> Result<Self, CgsWindowError> {
        unsafe {
            let connection = *G_CONNECTION;

            let frame_region = CFRegion::from_rect(&frame).map_err(CgsWindowError::Region)?;
            let empty_region = CFRegion::empty().map_err(CgsWindowError::Region)?;

            let mut tags: u64 = (1 << 1) | (1 << 9);

            let mut wid: WindowId = 0;
            cg_ok(SLSNewWindowWithOpaqueShapeAndContext(
                connection,
                2,
                frame_region.as_ptr(),
                empty_region.as_ptr(),
                13,
                &mut tags,
                0.0,
                0.0,
                TAG_BITSET_LEN,
                &mut wid,
                ptr::null_mut(),
            ))
            .map_err(CgsWindowError::Window)?;

            let window = Self {
                id: wid,
                connection,
                owned: true,
            };
            window.set_resolution(1.0)?;
            Ok(window)
        }
    }

    /// Creates a transparent window ready for a directly bound compositor surface.
    pub fn new_compositor(frame: CGRect, corner_radius: f64) -> Result<Self, CgsWindowError> {
        unsafe {
            let connection = *G_CONNECTION;
            let frame_region = CFRegion::from_rounded_rect(frame.size, corner_radius)
                .map_err(CgsWindowError::Region)?;
            let empty_region = CFRegion::empty().map_err(CgsWindowError::Region)?;
            let mut tags: u64 = (1 << 1) | (1 << 9) | (1 << 16);
            let mut wid: WindowId = 0;

            cg_ok(SLSNewWindowWithOpaqueShapeAndContext(
                connection,
                2,
                frame_region.as_ptr(),
                empty_region.as_ptr(),
                COMPOSITOR_WINDOW_OPTIONS,
                &mut tags,
                frame.origin.x as f32,
                frame.origin.y as f32,
                TAG_BITSET_LEN,
                &mut wid,
                ptr::null_mut(),
            ))
            .map_err(CgsWindowError::Window)?;

            let window = Self {
                id: wid,
                connection,
                owned: true,
            };
            window.set_resolution(2.0)?;
            window.set_opacity(false)?;
            window.clear_backing(frame.size)?;
            Ok(window)
        }
    }

    #[inline]
    pub fn id(&self) -> WindowId { self.id }

    #[inline]
    pub fn into_unowned(mut self) -> Self {
        self.owned = false;
        self
    }

    #[inline]
    pub fn from_existing(id: WindowId) -> Self {
        Self {
            id,
            connection: *G_CONNECTION,
            owned: false,
        }
    }

    #[inline]
    pub fn set_alpha(&self, alpha: f32) -> Result<(), CgsWindowError> {
        unsafe { cg_ok(SLSSetWindowAlpha(self.connection, self.id, alpha)) }
            .map_err(CgsWindowError::Alpha)
    }

    #[inline]
    pub fn set_opacity(&self, opaque: bool) -> Result<(), CgsWindowError> {
        unsafe { cg_ok(SLSSetWindowOpacity(self.connection, self.id, opaque)) }
            .map_err(CgsWindowError::Alpha)
    }

    #[inline]
    pub fn set_blur(&self, radius: i32, style: Option<i32>) -> Result<(), CgsWindowError> {
        unsafe {
            cg_ok(if let Some(style) = style {
                SLSSetWindowBackgroundBlurRadiusStyle(self.connection, self.id, radius, style)
            } else {
                SLSSetWindowBackgroundBlurRadius(self.connection, self.id, radius)
            })
        }
        .map_err(CgsWindowError::Blur)
    }

    #[inline]
    pub fn set_level(&self, level: i32) -> Result<(), CgsWindowError> {
        unsafe { cg_ok(SLSSetWindowLevel(self.connection, self.id, level)) }
            .map_err(CgsWindowError::Level)?;
        unsafe { cg_ok(SLSSetWindowSubLevel(self.connection, self.id, DEFAULT_SUBLEVEL)) }
            .map_err(CgsWindowError::Level)
    }

    #[inline]
    pub fn set_sublevel(&self, sublevel: i32) -> Result<(), CgsWindowError> {
        unsafe { cg_ok(SLSSetWindowSubLevel(self.connection, self.id, sublevel)) }
            .map_err(CgsWindowError::Level)
    }

    pub fn set_shape(&self, frame: CGRect) -> Result<(), CgsWindowError> {
        unsafe {
            let offset = frame.origin;
            let size_rect = CGRect::new(CGPoint::new(0.0, 0.0), frame.size);
            let region = CFRegion::from_rect(&size_rect).map_err(CgsWindowError::Region)?;
            let result = cg_ok(SLSSetWindowShape(
                self.connection,
                self.id,
                offset.x as f32,
                offset.y as f32,
                region.as_ptr(),
            ))
            .map_err(CgsWindowError::Shape);
            drop(region);
            result
        }
    }

    #[inline]
    pub fn set_tags(&self, tags: u64) -> Result<(), CgsWindowError> {
        unsafe {
            let mut t = tags;
            cg_ok(SLSSetWindowTags(
                self.connection,
                self.id,
                &mut t,
                TAG_BITSET_LEN,
            ))
            .map_err(CgsWindowError::Tags)
        }
    }

    #[inline]
    pub fn clear_tags(&self, tags: u64) -> Result<(), CgsWindowError> {
        unsafe {
            let mut t = tags;
            cg_ok(SLSClearWindowTags(
                self.connection,
                self.id,
                &mut t,
                TAG_BITSET_LEN,
            ))
            .map_err(CgsWindowError::Tags)
        }
    }

    #[inline]
    pub fn order_above(&self, relative: Option<WindowId>) -> Result<(), CgsWindowError> {
        let rel = relative.unwrap_or(0);
        unsafe {
            cg_ok(SLSOrderWindow(
                self.connection,
                self.id,
                1, // kCGSOrderAbove
                rel,
            ))
        }
        .map_err(CgsWindowError::Level)
    }

    #[inline]
    pub fn order_below(&self, relative: Option<WindowId>) -> Result<(), CgsWindowError> {
        let rel = relative.unwrap_or(0);
        unsafe {
            cg_ok(SLSOrderWindow(
                self.connection,
                self.id,
                -1, // kCGSOrderBelow
                rel,
            ))
        }
        .map_err(CgsWindowError::Level)
    }

    #[inline]
    pub fn order_out(&self) -> Result<(), CgsWindowError> {
        unsafe {
            cg_ok(SLSOrderWindow(
                self.connection,
                self.id,
                0, // kCGSOrderOut
                0,
            ))
        }
        .map_err(CgsWindowError::Level)
    }

    #[inline]
    pub fn set_property<T: Type>(
        &self,
        key: CFRetained<CFString>,
        value: CFRetained<T>,
    ) -> Result<(), CgsWindowError> {
        unsafe {
            cg_ok(SLSSetWindowProperty(
                self.connection,
                self.id,
                CFRetained::<CFString>::as_ptr(&key).as_ptr(),
                CFRetained::<T>::as_ptr(&value).as_ptr() as *mut CFType,
            ))
            .map_err(CgsWindowError::Property)
        }
    }

    #[inline]
    pub fn set_resolution(&self, scale: f64) -> Result<(), CgsWindowError> {
        unsafe { cg_ok(SLSSetWindowResolution(self.connection, self.id, scale)) }
            .map_err(CgsWindowError::Resolution)
    }

    /// Clear WindowServer's freshly allocated backing store before the first
    /// Core Animation surface is attached. On current macOS releases the
    /// uninitialized store can otherwise be composited as an opaque white
    /// rectangle behind an otherwise transparent CA layer tree.
    pub fn clear_backing(&self, size: CGSize) -> Result<(), CgsWindowError> {
        let context = unsafe { SLWindowContextCreate(self.connection, self.id, ptr::null_mut()) };
        let context = NonNull::new(context).ok_or(CgsWindowError::Surface(CGError(1000)))?;
        let context = unsafe { CFRetained::from_raw(context) };
        CGContext::clear_rect(Some(&context), CGRect::new(CGPoint::new(0.0, 0.0), size));
        CGContext::flush(Some(&context));
        drop(context);

        // Some macOS versions reject this notification even though flushing the
        // CGContext succeeded, so it is deliberately best-effort.
        let _ = unsafe { SLSFlushWindowContentRegion(self.connection, self.id, ptr::null_mut()) };
        Ok(())
    }
}

impl Drop for CgsWindow {
    fn drop(&mut self) {
        if !self.owned {
            return;
        }
        unsafe {
            if let Err(err) = cg_ok(SLSReleaseWindow(self.connection, self.id)) {
                tracing::warn!(error=?err, id=self.id, "failed to release CGS window");
            }
        }
    }
}