| /* GdkMacosLayer.c |
| * |
| * Copyright © 2022 Red Hat, Inc. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| */ |
| |
| #include "config.h" |
| |
| #import "GdkMacosLayer.h" |
| #import "GdkMacosTile.h" |
| |
| @protocol CanSetContentsOpaque |
| - (void)setContentsOpaque:(BOOL)mask; |
| @end |
| |
| @implementation GdkMacosLayer |
| |
| #define TILE_MAX_SIZE 128 |
| #define TILE_EDGE_MAX_SIZE 512 |
| |
| static CGAffineTransform flipTransform; |
| static gboolean hasFlipTransform; |
| |
| typedef struct |
| { |
| GdkMacosTile *tile; |
| cairo_rectangle_int_t cr_area; |
| CGRect area; |
| guint opaque : 1; |
| } TileInfo; |
| |
| typedef struct |
| { |
| const cairo_region_t *region; |
| guint n_rects; |
| guint iter; |
| cairo_rectangle_int_t rect; |
| cairo_rectangle_int_t stash; |
| guint finished : 1; |
| } Tiler; |
| |
| static void |
| tiler_init (Tiler *tiler, |
| const cairo_region_t *region) |
| { |
| memset (tiler, 0, sizeof *tiler); |
| |
| if (region == NULL) |
| { |
| tiler->finished = TRUE; |
| return; |
| } |
| |
| tiler->region = region; |
| tiler->n_rects = cairo_region_num_rectangles (region); |
| |
| if (tiler->n_rects > 0) |
| cairo_region_get_rectangle (region, 0, &tiler->rect); |
| else |
| tiler->finished = TRUE; |
| } |
| |
| static gboolean |
| tiler_next (Tiler *tiler, |
| cairo_rectangle_int_t *tile, |
| int max_size) |
| { |
| if (tiler->finished) |
| return FALSE; |
| |
| if (tiler->rect.width == 0 || tiler->rect.height == 0) |
| { |
| tiler->iter++; |
| |
| if (tiler->iter >= tiler->n_rects) |
| { |
| tiler->finished = TRUE; |
| return FALSE; |
| } |
| |
| cairo_region_get_rectangle (tiler->region, tiler->iter, &tiler->rect); |
| } |
| |
| /* If the next rectangle is too tall, slice the bottom off to |
| * leave just the height we want into tiler->stash. |
| */ |
| if (tiler->rect.height > max_size) |
| { |
| tiler->stash = tiler->rect; |
| tiler->stash.y += max_size; |
| tiler->stash.height -= max_size; |
| tiler->rect.height = max_size; |
| } |
| |
| /* Now we can take the next horizontal slice */ |
| tile->x = tiler->rect.x; |
| tile->y = tiler->rect.y; |
| tile->height = tiler->rect.height; |
| tile->width = MIN (max_size, tiler->rect.width); |
| |
| tiler->rect.x += tile->width; |
| tiler->rect.width -= tile->width; |
| |
| if (tiler->rect.width == 0) |
| { |
| tiler->rect = tiler->stash; |
| tiler->stash.width = tiler->stash.height = 0; |
| } |
| |
| return TRUE; |
| } |
| |
| static inline CGRect |
| toCGRect (const cairo_rectangle_int_t *rect) |
| { |
| return CGRectMake (rect->x, rect->y, rect->width, rect->height); |
| } |
| |
| static inline cairo_rectangle_int_t |
| fromCGRect (const CGRect rect) |
| { |
| return (cairo_rectangle_int_t) { |
| rect.origin.x, |
| rect.origin.y, |
| rect.size.width, |
| rect.size.height |
| }; |
| } |
| |
| -(id)init |
| { |
| if (!hasFlipTransform) |
| { |
| hasFlipTransform = TRUE; |
| flipTransform = CGAffineTransformMakeScale (1, -1); |
| } |
| |
| self = [super init]; |
| |
| if (self == NULL) |
| return NULL; |
| |
| self->_layoutInvalid = TRUE; |
| |
| [self setContentsGravity:kCAGravityCenter]; |
| [self setContentsScale:1.0f]; |
| [self setGeometryFlipped:YES]; |
| |
| return self; |
| } |
| |
| -(BOOL)isOpaque |
| { |
| return NO; |
| } |
| |
| -(void)_applyLayout:(GArray *)tiles |
| { |
| CGAffineTransform transform; |
| GArray *prev; |
| gboolean exhausted; |
| guint j = 0; |
| |
| if (self->_isFlipped) |
| transform = flipTransform; |
| else |
| transform = CGAffineTransformIdentity; |
| |
| prev = g_steal_pointer (&self->_tiles); |
| self->_tiles = tiles; |
| exhausted = prev == NULL; |
| |
| /* Try to use existing CALayer to avoid creating new layers |
| * as that can be rather expensive. |
| */ |
| for (guint i = 0; i < tiles->len; i++) |
| { |
| TileInfo *info = &g_array_index (tiles, TileInfo, i); |
| |
| if (!exhausted) |
| { |
| TileInfo *other = NULL; |
| |
| for (; j < prev->len; j++) |
| { |
| other = &g_array_index (prev, TileInfo, j); |
| |
| if (other->opaque == info->opaque) |
| { |
| j++; |
| break; |
| } |
| |
| other = NULL; |
| } |
| |
| if (other != NULL) |
| { |
| info->tile = g_steal_pointer (&other->tile); |
| [info->tile setFrame:info->area]; |
| [info->tile setAffineTransform:transform]; |
| continue; |
| } |
| } |
| |
| info->tile = [GdkMacosTile layer]; |
| |
| [info->tile setAffineTransform:transform]; |
| [info->tile setContentsScale:1.0f]; |
| [info->tile setOpaque:info->opaque]; |
| [(id<CanSetContentsOpaque>)info->tile setContentsOpaque:info->opaque]; |
| [info->tile setFrame:info->area]; |
| |
| [self addSublayer:info->tile]; |
| } |
| |
| /* Release all of our old layers */ |
| if (prev != NULL) |
| { |
| for (guint i = 0; i < prev->len; i++) |
| { |
| TileInfo *info = &g_array_index (prev, TileInfo, i); |
| |
| if (info->tile != NULL) |
| [info->tile removeFromSuperlayer]; |
| } |
| |
| g_array_unref (prev); |
| } |
| } |
| |
| -(void)layoutSublayers |
| { |
| Tiler tiler; |
| GArray *ar; |
| cairo_region_t *transparent; |
| cairo_rectangle_int_t rect; |
| int max_size; |
| |
| if (!self->_inSwapBuffer) |
| return; |
| |
| self->_layoutInvalid = FALSE; |
| |
| ar = g_array_sized_new (FALSE, FALSE, sizeof (TileInfo), 32); |
| |
| rect = fromCGRect ([self bounds]); |
| rect.x = rect.y = 0; |
| |
| /* Calculate the transparent region (edges usually) */ |
| transparent = cairo_region_create_rectangle (&rect); |
| if (self->_opaqueRegion) |
| cairo_region_subtract (transparent, self->_opaqueRegion); |
| |
| self->_opaque = cairo_region_is_empty (transparent); |
| |
| /* If we have transparent borders around the opaque region, then |
| * we are okay with a bit larger tiles since they don't change |
| * all that much and are generally small in width. |
| */ |
| if (!self->_opaque && |
| self->_opaqueRegion && |
| !cairo_region_is_empty (self->_opaqueRegion)) |
| max_size = TILE_EDGE_MAX_SIZE; |
| else |
| max_size = TILE_MAX_SIZE; |
| |
| /* Track transparent children */ |
| tiler_init (&tiler, transparent); |
| while (tiler_next (&tiler, &rect, max_size)) |
| { |
| TileInfo *info; |
| |
| g_array_set_size (ar, ar->len+1); |
| |
| info = &g_array_index (ar, TileInfo, ar->len-1); |
| info->tile = NULL; |
| info->opaque = FALSE; |
| info->cr_area = rect; |
| info->area = toCGRect (&info->cr_area); |
| } |
| |
| /* Track opaque children */ |
| tiler_init (&tiler, self->_opaqueRegion); |
| while (tiler_next (&tiler, &rect, TILE_MAX_SIZE)) |
| { |
| TileInfo *info; |
| |
| g_array_set_size (ar, ar->len+1); |
| |
| info = &g_array_index (ar, TileInfo, ar->len-1); |
| info->tile = NULL; |
| info->opaque = TRUE; |
| info->cr_area = rect; |
| info->area = toCGRect (&info->cr_area); |
| } |
| |
| cairo_region_destroy (transparent); |
| |
| [self _applyLayout:g_steal_pointer (&ar)]; |
| [super layoutSublayers]; |
| } |
| |
| -(void)setFrame:(NSRect)frame |
| { |
| if (frame.size.width != self.bounds.size.width || |
| frame.size.height != self.bounds.size.height) |
| { |
| self->_layoutInvalid = TRUE; |
| [self setNeedsLayout]; |
| } |
| |
| [super setFrame:frame]; |
| } |
| |
| -(void)setOpaqueRegion:(const cairo_region_t *)opaqueRegion |
| { |
| g_clear_pointer (&self->_opaqueRegion, cairo_region_destroy); |
| self->_opaqueRegion = cairo_region_copy (opaqueRegion); |
| self->_layoutInvalid = TRUE; |
| |
| [self setNeedsLayout]; |
| } |
| |
| -(void)swapBuffer:(GdkMacosBuffer *)buffer withDamage:(const cairo_region_t *)damage |
| { |
| IOSurfaceRef ioSurface = _gdk_macos_buffer_get_native (buffer); |
| gboolean flipped = _gdk_macos_buffer_get_flipped (buffer); |
| double scale = _gdk_macos_buffer_get_device_scale (buffer); |
| double width = _gdk_macos_buffer_get_width (buffer) / scale; |
| double height = _gdk_macos_buffer_get_height (buffer) / scale; |
| |
| if (flipped != self->_isFlipped) |
| { |
| self->_isFlipped = flipped; |
| self->_layoutInvalid = TRUE; |
| } |
| |
| if (self->_layoutInvalid) |
| { |
| self->_inSwapBuffer = TRUE; |
| [self layoutSublayers]; |
| self->_inSwapBuffer = FALSE; |
| } |
| |
| if (self->_tiles == NULL) |
| return; |
| |
| for (guint i = 0; i < self->_tiles->len; i++) |
| { |
| const TileInfo *info = &g_array_index (self->_tiles, TileInfo, i); |
| cairo_region_overlap_t overlap; |
| CGRect area; |
| |
| overlap = cairo_region_contains_rectangle (damage, &info->cr_area); |
| if (overlap == CAIRO_REGION_OVERLAP_OUT) |
| continue; |
| |
| area.origin.x = info->area.origin.x / width; |
| area.size.width = info->area.size.width / width; |
| area.size.height = info->area.size.height / height; |
| |
| if (flipped) |
| area.origin.y = (height - info->area.origin.y - info->area.size.height) / height; |
| else |
| area.origin.y = info->area.origin.y / height; |
| |
| [info->tile swapBuffer:ioSurface withRect:area]; |
| } |
| } |
| |
| @end |