diff options
Diffstat (limited to 'subprojects/libhandy/src/hdy-window-mixin.c')
-rw-r--r-- | subprojects/libhandy/src/hdy-window-mixin.c | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/subprojects/libhandy/src/hdy-window-mixin.c b/subprojects/libhandy/src/hdy-window-mixin.c new file mode 100644 index 0000000..d55536c --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-mixin.c @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-cairo-private.h" +#include "hdy-deck.h" +#include "hdy-nothing-private.h" +#include "hdy-window-mixin-private.h" + +typedef enum { + HDY_CORNER_TOP_LEFT, + HDY_CORNER_TOP_RIGHT, + HDY_CORNER_BOTTOM_LEFT, + HDY_CORNER_BOTTOM_RIGHT, + HDY_N_CORNERS, +} HdyCorner; + +/** + * PRIVATE:hdy-window-mixin + * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow + * @title: HdyWindowMixin + * @See_also: #HdyApplicationWindow, #HdyWindow + * @stability: Private + * + * The HdyWindowMixin object contains the implementation of the HdyWindow and + * HdyApplicationWindow classes, providing a way to make a GtkWindow subclass + * that has masked window corners on all sides and no titlebar by default, + * allowing for more freedom with how to handle the titlebar for applications. + * + * Since: 1.0 + */ + +struct _HdyWindowMixin +{ + GObject parent; + + GtkWindow *window; + GtkWindowClass *klass; + + GtkWidget *content; + GtkWidget *titlebar; + cairo_surface_t *masks[HDY_N_CORNERS]; + gint last_border_radius; + + GtkStyleContext *decoration_context; + GtkStyleContext *overlay_context; + + GtkWidget *child; +}; + +G_DEFINE_TYPE (HdyWindowMixin, hdy_window_mixin, G_TYPE_OBJECT) + +static GtkStyleContext * +create_child_context (HdyWindowMixin *self) +{ + GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + GtkStyleContext *child = gtk_style_context_new (); + + gtk_style_context_set_parent (child, parent); + gtk_style_context_set_screen (child, gtk_style_context_get_screen (parent)); + gtk_style_context_set_frame_clock (child, gtk_style_context_get_frame_clock (parent)); + + g_signal_connect_object (child, + "changed", + G_CALLBACK (gtk_widget_queue_draw), + self->window, + G_CONNECT_SWAPPED); + + return child; +} + +static void +update_child_context (HdyWindowMixin *self, + GtkStyleContext *context, + const gchar *name) +{ + g_autoptr (GtkWidgetPath) path = gtk_widget_path_new (); + GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + gint position; + + gtk_widget_path_append_for_widget (path, GTK_WIDGET (self->window)); + position = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET); + gtk_widget_path_iter_set_object_name (path, position, name); + + gtk_style_context_set_path (context, path); + gtk_style_context_set_state (context, gtk_style_context_get_state (parent)); +} + +static void +style_changed_cb (HdyWindowMixin *self) +{ + update_child_context (self, self->decoration_context, "decoration"); + update_child_context (self, self->overlay_context, "decoration-overlay"); +} + +static gboolean +window_state_event_cb (HdyWindowMixin *self, + GdkEvent *event, + GtkWidget *widget) +{ + style_changed_cb (self); + + return GDK_EVENT_PROPAGATE; +} + +static void +size_allocate_cb (HdyWindowMixin *self, + GtkAllocation *alloc) +{ + /* We don't want to allow any other titlebar */ + if (gtk_window_get_titlebar (self->window) != self->titlebar) + g_error ("gtk_window_set_titlebar() is not supported for HdyWindow"); +} + +static gboolean +is_fullscreen (HdyWindowMixin *self) +{ + GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self->window)); + + return !!(gdk_window_get_state (window) & GDK_WINDOW_STATE_FULLSCREEN); +} + +static gboolean +supports_client_shadow (HdyWindowMixin *self) +{ + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + + /* + * GtkWindow adds this when it can't draw proper decorations, e.g. on a + * non-composited WM on X11. This is documented, so we can rely on this + * instead of copying the (pretty extensive) check. + */ + return !gtk_style_context_has_class (context, "solid-csd"); +} + +static void +max_borders (GtkBorder *one, + GtkBorder *two) +{ + one->top = MAX (one->top, two->top); + one->right = MAX (one->right, two->right); + one->bottom = MAX (one->bottom, two->bottom); + one->left = MAX (one->left, two->left); +} + +static void +get_shadow_width (HdyWindowMixin *self, + GtkStyleContext *context, + GtkBorder *shadow_width) +{ + GtkStateFlags state; + GtkBorder margin = { 0 }; + GtkAllocation content_alloc, alloc; + GtkWidget *titlebar; + + *shadow_width = margin; + + if (!gtk_window_get_decorated (self->window)) + return; + + if (gtk_window_is_maximized (self->window) || + is_fullscreen (self)) + return; + + if (!gtk_widget_is_toplevel (GTK_WIDGET (self->window))) + return; + + state = gtk_style_context_get_state (context); + + gtk_style_context_get_margin (context, state, &margin); + + gtk_widget_get_allocation (GTK_WIDGET (self->window), &alloc); + gtk_widget_get_allocation (self->content, &content_alloc); + + titlebar = gtk_window_get_titlebar (self->window); + if (titlebar && gtk_widget_get_visible (titlebar)) { + GtkAllocation titlebar_alloc; + + gtk_widget_get_allocation (titlebar, &titlebar_alloc); + + content_alloc.y = titlebar_alloc.y; + content_alloc.height += titlebar_alloc.height; + } + + /* + * Since we can't get shadow extents the normal way, + * we have to compare window and content allocation instead. + */ + shadow_width->left = content_alloc.x - alloc.x; + shadow_width->right = alloc.width - content_alloc.width - content_alloc.x; + shadow_width->top = content_alloc.y - alloc.y; + shadow_width->bottom = alloc.height - content_alloc.height - content_alloc.y; + + max_borders (shadow_width, &margin); +} + +static void +create_masks (HdyWindowMixin *self, + cairo_t *cr, + gint border_radius) +{ + gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self->window)); + gdouble radius_correction = 0.5 / scale_factor; + gdouble r = border_radius - radius_correction; + gint i; + + for (i = 0; i < HDY_N_CORNERS; i++) + g_clear_pointer (&self->masks[i], cairo_surface_destroy); + + if (r <= 0) + return; + + for (i = 0; i < HDY_N_CORNERS; i++) { + g_autoptr (cairo_t) mask_cr = NULL; + + self->masks[i] = + cairo_surface_create_similar_image (cairo_get_target (cr), + CAIRO_FORMAT_A8, + border_radius * scale_factor, + border_radius * scale_factor); + + mask_cr = cairo_create (self->masks[i]); + + cairo_scale (mask_cr, scale_factor, scale_factor); + cairo_set_source_rgb (mask_cr, 0, 0, 0); + cairo_arc (mask_cr, + (i % 2 == 0) ? r : radius_correction, + (i / 2 == 0) ? r : radius_correction, + r, + 0, G_PI * 2); + cairo_fill (mask_cr); + } +} + +void +hdy_window_mixin_add (HdyWindowMixin *self, + GtkWidget *widget) +{ + if (GTK_IS_POPOVER (widget)) + GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window), + widget); + else { + g_return_if_fail (self->child == NULL); + + self->child = widget; + gtk_container_add (GTK_CONTAINER (self->content), widget); + } +} + +void +hdy_window_mixin_remove (HdyWindowMixin *self, + GtkWidget *widget) +{ + GtkWidget *titlebar = gtk_window_get_titlebar (self->window); + + if (widget == self->content || + widget == titlebar || + GTK_IS_POPOVER (widget)) + GTK_CONTAINER_CLASS (self->klass)->remove (GTK_CONTAINER (self->window), + widget); + else if (widget == self->child) { + self->child = NULL; + gtk_container_remove (GTK_CONTAINER (self->content), widget); + } +} + +void +hdy_window_mixin_forall (HdyWindowMixin *self, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + if (include_internals) { + GTK_CONTAINER_CLASS (self->klass)->forall (GTK_CONTAINER (self->window), + include_internals, + callback, + callback_data); + + return; + } + + if (self->child) + (*callback) (self->child, callback_data); +} + +typedef struct { + HdyWindowMixin *self; + cairo_t *cr; +} HdyWindowMixinDrawData; + +static void +draw_popover_cb (GtkWidget *child, + HdyWindowMixinDrawData *data) +{ + HdyWindowMixin *self = data->self; + GdkWindow *window; + cairo_t *cr = data->cr; + + if (child == self->content || + child == gtk_window_get_titlebar (self->window) || + !gtk_widget_get_visible (child) || + !gtk_widget_get_child_visible (child)) + return; + + window = gtk_widget_get_window (child); + + if (gtk_widget_get_has_window (child)) + window = gdk_window_get_parent (window); + + if (!gtk_cairo_should_draw_window (cr, window)) + return; + + gtk_container_propagate_draw (GTK_CONTAINER (self->window), child, cr); +} + +static inline void +mask_corner (HdyWindowMixin *self, + cairo_t *cr, + gint scale_factor, + gint corner, + gint x, + gint y) +{ + cairo_save (cr); + cairo_scale (cr, 1.0 / scale_factor, 1.0 / scale_factor); + cairo_mask_surface (cr, + self->masks[corner], + x * scale_factor, + y * scale_factor); + cairo_restore (cr); +} + +gboolean +hdy_window_mixin_draw (HdyWindowMixin *self, + cairo_t *cr) +{ + HdyWindowMixinDrawData data; + GtkWidget *widget = GTK_WIDGET (self->window); + GdkWindow *window = gtk_widget_get_window (widget); + + if (gtk_cairo_should_draw_window (cr, window)) { + GtkStyleContext *context; + gboolean should_mask_corners; + GdkRectangle clip = { 0 }; + gint width, height, x, y, w, h, r, scale_factor; + GtkWidget *titlebar; + g_autoptr (cairo_surface_t) surface = NULL; + g_autoptr (cairo_t) surface_cr = NULL; + GtkBorder shadow; + + /* Use the parent drawing unless we have a reason to use masking */ + if (!gtk_window_get_decorated (self->window) || + !supports_client_shadow (self) || + is_fullscreen (self)) + return GTK_WIDGET_CLASS (self->klass)->draw (GTK_WIDGET (self->window), cr); + + context = gtk_widget_get_style_context (widget); + + get_shadow_width (self, self->decoration_context, &shadow); + + width = gtk_widget_get_allocated_width (widget); + height = gtk_widget_get_allocated_height (widget); + + x = shadow.left; + y = shadow.top; + w = width - shadow.left - shadow.right; + h = height - shadow.top - shadow.bottom; + + gtk_style_context_get (context, + gtk_style_context_get_state (self->decoration_context), + GTK_STYLE_PROPERTY_BORDER_RADIUS, &r, + NULL); + + r = CLAMP (r, 0, MIN (w / 2, h / 2)); + + if (!gdk_cairo_get_clip_rectangle (cr, &clip)) { + clip.x = 0; + clip.y = 0; + clip.width = w; + clip.height = h; + } + + gtk_render_background (self->decoration_context, cr, x, y, w, h); + gtk_render_frame (self->decoration_context, cr, x, y, w, h); + + cairo_save (cr); + + scale_factor = gtk_widget_get_scale_factor (widget); + + if (r * scale_factor != self->last_border_radius) { + create_masks (self, cr, r); + self->last_border_radius = r * scale_factor; + } + + should_mask_corners = !gtk_window_is_maximized (self->window) && + r > 0 && + ((clip.x < x + r && clip.y < y + r) || + (clip.x < x + r && clip.y + clip.height > y + h - r) || + (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) || + (clip.x + clip.width > x + w - r && clip.y < y + r)); + + + if (should_mask_corners) { + surface = gdk_window_create_similar_surface (window, + CAIRO_CONTENT_COLOR_ALPHA, + MAX (clip.width, 1), + MAX (clip.height, 1)); + surface_cr = cairo_create (surface); + cairo_surface_set_device_offset (surface, -clip.x * scale_factor, -clip.y * scale_factor); + } else { + surface_cr = cairo_reference (cr); + } + + if (!gtk_widget_get_app_paintable (widget)) { + gtk_render_background (context, surface_cr, x, y, w, h); + gtk_render_frame (context, surface_cr, x, y, w, h); + } + + titlebar = gtk_window_get_titlebar (self->window); + + gtk_container_propagate_draw (GTK_CONTAINER (self->window), self->content, surface_cr); + gtk_container_propagate_draw (GTK_CONTAINER (self->window), titlebar, surface_cr); + + gtk_render_background (self->overlay_context, surface_cr, x, y, w, h); + gtk_render_frame (self->overlay_context, surface_cr, x, y, w, h); + + if (should_mask_corners) { + cairo_set_source_surface (cr, surface, 0, 0); + + cairo_rectangle (cr, x + r, y, w - r * 2, r); + cairo_rectangle (cr, x + r, y + h - r, w - r * 2, r); + cairo_rectangle (cr, x, y + r, w, h - r * 2); + cairo_fill (cr); + + if (clip.x < x + r && clip.y < y + r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_TOP_LEFT, x, y); + + if (clip.x + clip.width > x + w - r && clip.y < y + r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_TOP_RIGHT, x + w - r, y); + + if (clip.x < x + r && clip.y + clip.height > y + h - r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_BOTTOM_LEFT, x, y + h - r); + + if (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_BOTTOM_RIGHT, x + w - r, y + h - r); + + cairo_surface_flush (surface); + } + + cairo_restore (cr); + } + + data.self = self; + data.cr = cr; + gtk_container_forall (GTK_CONTAINER (self->window), + (GtkCallback) draw_popover_cb, + &data); + + return GDK_EVENT_PROPAGATE; +} + +void +hdy_window_mixin_destroy (HdyWindowMixin *self) +{ + if (self->titlebar) { + hdy_window_mixin_remove (self, self->titlebar); + self->titlebar = NULL; + } + + if (self->content) { + hdy_window_mixin_remove (self, self->content); + self->content = NULL; + self->child = NULL; + } + + GTK_WIDGET_CLASS (self->klass)->destroy (GTK_WIDGET (self->window)); +} + +void +hdy_window_mixin_buildable_add_child (HdyWindowMixin *self, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + GtkBuildable *buildable = GTK_BUILDABLE (self->window); + + if (!type) + gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type); +} + +static void +hdy_window_mixin_finalize (GObject *object) +{ + HdyWindowMixin *self = (HdyWindowMixin *)object; + gint i; + + for (i = 0; i < HDY_N_CORNERS; i++) + g_clear_pointer (&self->masks[i], cairo_surface_destroy); + g_clear_object (&self->decoration_context); + g_clear_object (&self->overlay_context); + + G_OBJECT_CLASS (hdy_window_mixin_parent_class)->finalize (object); +} + +static void +hdy_window_mixin_class_init (HdyWindowMixinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = hdy_window_mixin_finalize; +} + +static void +hdy_window_mixin_init (HdyWindowMixin *self) +{ +} + +HdyWindowMixin * +hdy_window_mixin_new (GtkWindow *window, + GtkWindowClass *klass) +{ + HdyWindowMixin *self; + GtkStyleContext *context; + + g_return_val_if_fail (GTK_IS_WINDOW (window), NULL); + g_return_val_if_fail (GTK_IS_WINDOW_CLASS (klass), NULL); + g_return_val_if_fail (GTK_IS_BUILDABLE (window), NULL); + + self = g_object_new (HDY_TYPE_WINDOW_MIXIN, NULL); + + self->window = window; + self->klass = klass; + + gtk_widget_add_events (GTK_WIDGET (window), GDK_STRUCTURE_MASK); + + g_signal_connect_object (window, + "style-updated", + G_CALLBACK (style_changed_cb), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (window, + "window-state-event", + G_CALLBACK (window_state_event_cb), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + + g_signal_connect_object (window, + "size-allocate", + G_CALLBACK (size_allocate_cb), + self, + G_CONNECT_SWAPPED); + + self->decoration_context = create_child_context (self); + self->overlay_context = create_child_context (self); + + style_changed_cb (self); + + self->content = hdy_deck_new (); + gtk_widget_set_vexpand (self->content, TRUE); + gtk_widget_show (self->content); + GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window), + self->content); + + self->titlebar = hdy_nothing_new (); + gtk_widget_set_no_show_all (self->titlebar, TRUE); + gtk_window_set_titlebar (self->window, self->titlebar); + + context = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + gtk_style_context_add_class (context, "unified"); + + return self; +} |