use core::ptr::NonNull;
use denise::{BufferAge, Frame, PixelFormat, Rect, Size, Surface, SurfaceError};
use objc2_core_foundation::{CFRetained, CGFloat, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapContextCreate, CGBitmapContextCreateImage, CGBitmapContextGetBytesPerRow,
CGBitmapContextGetData, CGColorSpace, CGContext, CGImageAlphaInfo, CGImageByteOrderInfo,
};
use crate::Error;
fn bitmap_info() -> u32 {
CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::NoneSkipFirst.0
}
pub struct ViewSurface {
context: CFRetained<CGContext>,
pixels: NonNull<u32>,
stride: u32,
size: Size,
scale_factor: f32,
}
impl ViewSurface {
pub fn new(size: Size, scale_factor: f32) -> Result<Self, Error> {
if size.is_empty() {
return Err(Error::EmptySurface);
}
let space = CGColorSpace::new_device_rgb().ok_or(Error::ColorSpace)?;
let context = unsafe {
CGBitmapContextCreate(
core::ptr::null_mut(),
size.width as usize,
size.height as usize,
8,
0,
Some(&space),
bitmap_info(),
)
}
.ok_or(Error::BitmapContext)?;
let data = CGBitmapContextGetData(Some(&context));
let pixels = NonNull::new(data.cast::<u32>()).ok_or(Error::BitmapContext)?;
let bytes_per_row = CGBitmapContextGetBytesPerRow(Some(&context));
if !bytes_per_row.is_multiple_of(4) {
return Err(Error::BitmapContext);
}
Ok(Self {
context,
pixels,
stride: (bytes_per_row / 4) as u32,
size,
scale_factor,
})
}
pub fn resize(&mut self, size: Size, scale_factor: f32) -> Result<bool, Error> {
if size == self.size && scale_factor == self.scale_factor {
return Ok(false);
}
*self = Self::new(size, scale_factor)?;
Ok(true)
}
#[inline]
pub const fn stride(&self) -> u32 {
self.stride
}
pub unsafe fn draw_into(&self, context: &CGContext, bounds: CGRect) {
let Some(image) = CGBitmapContextCreateImage(Some(&self.context)) else {
return;
};
CGContext::save_g_state(Some(context));
CGContext::translate_ctm(
Some(context),
0.0,
bounds.origin.y * 2.0 + bounds.size.height,
);
CGContext::scale_ctm(Some(context), 1.0, -1.0);
CGContext::draw_image(Some(context), bounds, Some(&image));
CGContext::restore_g_state(Some(context));
}
#[inline]
pub fn context(&self) -> &CGContext {
&self.context
}
pub fn damage_to_points(&self, rect: Rect) -> CGRect {
let scale = self.scale_factor.max(0.01) as CGFloat;
let x = (rect.x as CGFloat / scale).floor();
let y = (rect.y as CGFloat / scale).floor();
let right = ((rect.x + rect.width) as CGFloat / scale).ceil();
let bottom = ((rect.y + rect.height) as CGFloat / scale).ceil();
CGRect::new(CGPoint::new(x, y), CGSize::new(right - x, bottom - y))
}
}
impl Surface for ViewSurface {
fn size(&self) -> Size {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn format(&self) -> PixelFormat {
PixelFormat::Xrgb8888
}
fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
let len = self.stride as usize * self.size.height as usize;
let pixels = unsafe { core::slice::from_raw_parts_mut(self.pixels.as_ptr(), len) };
Frame::new(
pixels,
self.size,
self.stride,
PixelFormat::Xrgb8888,
BufferAge::Frames(1),
)
}
fn present(&mut self, _damage: &[Rect]) -> Result<(), SurfaceError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use denise::Color;
use denise_render::Canvas;
#[test]
fn core_graphics_picks_the_stride_and_it_is_not_the_width() {
let surface = ViewSurface::new(Size::new(100, 40), 1.0).expect("surface");
assert!(
surface.stride() >= 100,
"a stride below the width cannot hold a row"
);
assert_eq!(
surface.stride() * 4 % 4,
0,
"the pitch must be a whole number of words"
);
}
#[test]
fn drawing_lands_where_the_stride_says_it_does() {
let mut surface = ViewSurface::new(Size::new(64, 32), 1.0).expect("surface");
{
let mut frame = surface.acquire().expect("frame");
let mut canvas = Canvas::new(&mut frame);
canvas.clear(Color::from_rgb888(0x00FF00));
canvas.fill_rect(Rect::new(0, 0, 1, 1), Color::from_rgb888(0xFF0000));
}
let mut frame = surface.acquire().expect("frame");
let row = frame.row_mut(1).expect("second row");
assert_eq!(
row[0] & 0x00FF_FFFF,
0x00FF00,
"row 1 is not the clear colour, so the stride is wrong"
);
}
#[test]
fn a_flipped_context_draws_the_surface_the_right_way_up() {
use objc2_core_graphics::CGBitmapContextCreate;
const N: u32 = 8;
let mut source = ViewSurface::new(Size::new(N, N), 1.0).expect("source");
{
let mut frame = source.acquire().expect("frame");
let mut canvas = Canvas::new(&mut frame);
for y in 0..N as i32 {
let shade = (y as u32 * 16) << 16;
canvas.fill_rect(Rect::new(0, y, N as i32, 1), Color::from_rgb888(shade));
}
}
let space = CGColorSpace::new_device_rgb().expect("colour space");
let dest = unsafe {
CGBitmapContextCreate(
core::ptr::null_mut(),
N as usize,
N as usize,
8,
0,
Some(&space),
bitmap_info(),
)
}
.expect("destination context");
CGContext::translate_ctm(Some(&dest), 0.0, N as CGFloat);
CGContext::scale_ctm(Some(&dest), 1.0, -1.0);
let bounds = CGRect::new(
CGPoint::new(0.0, 0.0),
CGSize::new(N as CGFloat, N as CGFloat),
);
unsafe { source.draw_into(&dest, bounds) };
let dest_stride = CGBitmapContextGetBytesPerRow(Some(&dest)) / 4;
let dest_data = CGBitmapContextGetData(Some(&dest)).cast::<u32>();
assert!(!dest_data.is_null());
let drawn = unsafe { core::slice::from_raw_parts(dest_data, dest_stride * N as usize) };
let mut frame = source.acquire().expect("frame");
for y in 0..N {
let expected = frame.row_mut(y).expect("source row")[0] & 0x00FF_FFFF;
let actual = drawn[y as usize * dest_stride] & 0x00FF_FFFF;
assert_eq!(
actual, expected,
"row {y} came out as {actual:06X}, wanted {expected:06X} — \
the image is mirrored, so the panel renders upside down"
);
}
}
#[test]
fn an_empty_surface_is_refused_rather_than_allocated() {
assert!(ViewSurface::new(Size::new(0, 40), 1.0).is_err());
assert!(ViewSurface::new(Size::new(40, 0), 1.0).is_err());
}
#[test]
fn damage_rounds_outwards_on_a_retina_view() {
let surface = ViewSurface::new(Size::new(200, 100), 2.0).expect("surface");
let rect = surface.damage_to_points(Rect::new(3, 5, 4, 4));
assert_eq!(rect.origin.x, 1.0);
assert_eq!(rect.origin.y, 2.0);
assert_eq!(rect.size.width, 3.0);
assert_eq!(rect.size.height, 3.0);
}
}