GTK 3: be aware of the window's scale factor.

In GTK 3.10 and above, high-DPI support is arranged by each window
having a property called a 'scale factor', which translates logical
pixels as seen by most of the GTK API (widget and window sizes and
positions, coordinates in the "draw" event, etc) into the physical
pixels on the screen. This is handled more or less transparently,
except that one side effect is that your Cairo-based drawing code had
better be able to cope with that scaling without getting confused.

PuTTY's isn't, because we do all our serious drawing on a separate
Cairo surface we made ourselves, and then blit subrectangles of that
to the window during updates. This has two bad consequences. Firstly,
our surface has a size derived from what GTK told us the drawing area
size is, i.e. corresponding to GTK's _logical_ pixels, so when the
scale factor is (say) 2, our drawing takes place at half size and then
gets scaled up by the final blit in the draw event, making it look
blurry and unpleasant. Secondly, those final blits seem to end up
offset by half a pixel, so that a second blit over the same
subrectangle doesn't _quite_ completely wipe out the previously
blitted data - so there's a ghostly rectangle left behind everywhere
the cursor has been.

It's not that GTK doesn't _let_ you find out the scale factor; it's
just that it's in an out-of-the-way piece of API that you have to call
specially. So now we do: our backing surface is now created at a pixel
resolution matching the screen's real pixels, and we translate GTK's
scale factor into an ordinary cairo_scale() before we commence
drawing. So we still end up drawing the same text at the same size -
and this strategy also means that non-text elements like cursor
outlines and underlining will be scaled up with the screen DPI rather
than stubbornly staying one physical pixel thick - but now it's nice
and sharp at full screen resolution, and the subrectangle blits in the
draw event are back to affecting the exact set of pixels we expect
them to.

One silly consequence is that, immediately after removing the last
one, I've installed a handler for the GTK "configure-event" signal!
That's because the GTK 3 docs claim that that's how you get notified
that your scale factor has changed at run time (e.g. if you
reconfigure the scale factor of a whole monitor in the GNOME settings
dialog). Actually in practice I seem to find out via the "draw" event
before "configure" bothers to tell me, but now I've got a usefully
idempotent function for 'check whether the scale factor has changed
and sort it out if so', I don't see any harm in calling it from
anywhere it _might_ be useful.
This commit is contained in:
Simon Tatham 2018-05-11 07:53:05 +01:00
Родитель 1ca03a186f
Коммит 383302d70a
1 изменённых файлов: 87 добавлений и 7 удалений

Просмотреть файл

@ -149,7 +149,7 @@ struct gui_data {
#endif
int clipboard_ctrlshiftins, clipboard_ctrlshiftcv;
int font_width, font_height;
int width, height;
int width, height, scale;
int ignore_sbar;
int mouseptr_visible;
int busy_status;
@ -633,7 +633,7 @@ static void draw_backing_rect(struct gui_data *inst);
static void drawing_area_setup(struct gui_data *inst, int width, int height)
{
int w, h, need_size = 0;
int w, h, new_scale, need_size = 0;
/*
* See if the terminal size has changed, in which case we must
@ -649,20 +649,32 @@ static void drawing_area_setup(struct gui_data *inst, int width, int height)
need_size = 1;
}
#if GTK_CHECK_VERSION(3,10,0)
new_scale = gtk_widget_get_scale_factor(inst->area);
#else
new_scale = 1;
#endif
/*
* If the terminal size hasn't changed since the previous call to
* this function (in particular, if there has at least _been_ a
* previous call to this function), then, we can assume this event
* is spurious and do nothing further.
* If neither the terminal size nor the HiDPI scale factor has
* changed since the previous call to this function (and, in
* particular, if there has at least _been_ a previous call to
* this function), then, we can assume this event is spurious and
* do nothing further.
*/
if (!need_size && inst->drawing_area_setup_done)
if (inst->drawing_area_setup_done &&
!need_size && new_scale == inst->scale)
return;
inst->drawing_area_setup_done = TRUE;
inst->scale = new_scale;
{
int backing_w = w * inst->font_width + 2*inst->window_border;
int backing_h = h * inst->font_height + 2*inst->window_border;
backing_w *= inst->scale;
backing_h *= inst->scale;
#ifndef NO_BACKING_PIXMAPS
if (inst->pixmap) {
gdk_pixmap_unref(inst->pixmap);
@ -736,6 +748,30 @@ static void area_size_allocate(
drawing_area_setup(inst, alloc->width, alloc->height);
}
#if GTK_CHECK_VERSION(3,10,0)
static void area_check_scale(struct gui_data *inst)
{
if (inst->drawing_area_setup_done &&
inst->scale != gtk_widget_get_scale_factor(inst->area)) {
drawing_area_setup_simple(inst);
if (inst->term) {
term_invalidate(inst->term);
term_update(inst->term);
}
}
}
#endif
#if GTK_CHECK_VERSION(3,10,0)
static gboolean area_configured(
GtkWidget *widget, GdkEventConfigure *event, gpointer data)
{
struct gui_data *inst = (struct gui_data *)data;
area_check_scale(inst);
return FALSE;
}
#endif
#ifdef DRAW_TEXT_CAIRO
static void cairo_setup_dctx(struct draw_ctx *dctx)
{
@ -757,6 +793,16 @@ static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
{
struct gui_data *inst = (struct gui_data *)data;
#if GTK_CHECK_VERSION(3,10,0)
/*
* This may be the first we hear of the window scale having
* changed, in which case we must hastily reconstruct our backing
* surface before we copy the wrong one into the newly resized
* real window.
*/
area_check_scale(inst);
#endif
/*
* GTK3 window redraw: we always expect Cairo to be enabled, so
* that inst->surface exists, and pixmaps to be disabled, so that
@ -765,6 +811,33 @@ static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
*/
if (inst->surface) {
GdkRectangle dirtyrect;
cairo_surface_t *target_surface;
double orig_sx, orig_sy;
cairo_matrix_t m;
/*
* Furtle around in the Cairo setup to force the device scale
* back to 1, so that when we blit a collection of pixels from
* our backing surface into the window, they really are
* _pixels_ and not some confusing antialiased slightly-offset
* 2x2 rectangle of pixeloids.
*
* I have no idea whether GTK expects me not to mess with the
* device scale in the cairo_surface_t backing its window, so
* I carefully put it back when I've finished.
*
* In some GTK setups, the Cairo context we're given may not
* have a zero translation offset in its matrix, in which case
* we have to adjust that to compensate for the change of
* scale, or else the old translation offset (designed for the
* old scale) will be multiplied by the new scale instead and
* put everything in the wrong place.
*/
target_surface = cairo_get_target(cr);
cairo_get_matrix(cr, &m);
cairo_surface_get_device_scale(target_surface, &orig_sx, &orig_sy);
cairo_surface_set_device_scale(target_surface, 1.0, 1.0);
cairo_translate(cr, m.x0 * (orig_sx - 1.0), m.y0 * (orig_sy - 1.0));
gdk_cairo_get_clip_rectangle(cr, &dirtyrect);
@ -772,6 +845,8 @@ static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
cairo_rectangle(cr, dirtyrect.x, dirtyrect.y,
dirtyrect.width, dirtyrect.height);
cairo_fill(cr);
cairo_surface_set_device_scale(target_surface, orig_sx, orig_sy);
}
return TRUE;
@ -3442,6 +3517,7 @@ Context get_ctx(void *frontend)
* exist, and we draw to that first, regardless of whether we
* subsequently copy the results to inst->pixmap. */
dctx->uctx.u.cairo.cr = cairo_create(inst->surface);
cairo_scale(dctx->uctx.u.cairo.cr, inst->scale, inst->scale);
cairo_setup_dctx(dctx);
}
#endif
@ -5227,6 +5303,10 @@ void new_session_window(Conf *conf, const char *geometry_string)
G_CALLBACK(area_realised), inst);
g_signal_connect(G_OBJECT(inst->area), "size_allocate",
G_CALLBACK(area_size_allocate), inst);
#if GTK_CHECK_VERSION(3,10,0)
g_signal_connect(G_OBJECT(inst->area), "configure_event",
G_CALLBACK(area_configured), inst);
#endif
#if GTK_CHECK_VERSION(3,0,0)
g_signal_connect(G_OBJECT(inst->area), "draw",
G_CALLBACK(draw_area), inst);