mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2024-12-29 15:01:23 +00:00
df8e2bc0a0
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.
387 lines
9.1 KiB
C
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
|