gtk/gdk/macos/GdkMacosLayer.c
Christian Hergert df8e2bc0a0 macos: only invalidate tiles when size changes
If the size changes, we need to relayout the tiles. Otherwise we can keep
using what we had before. Generally, that shouldn't happen, but the
previous check was failing in a number of ways.
2022-02-22 12:01:29 -08:00

387 lines
9.1 KiB
C

/* 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