diff --git a/debian/libhandy-1-0.symbols b/debian/libhandy-1-0.symbols index c2af72f8116886e94dcd8b6265e7ae6164397105..a3c866f0ab6fbd597d66b19afff2d1bef1422ad1 100644 --- a/debian/libhandy-1-0.symbols +++ b/debian/libhandy-1-0.symbols @@ -356,6 +356,10 @@ libhandy-1.so.0 libhandy-1-0 #MINVER# hdy_tab_bar_set_inverted@LIBHANDY_1_0 1.2.0 hdy_tab_bar_set_start_action_widget@LIBHANDY_1_0 1.2.0 hdy_tab_bar_set_view@LIBHANDY_1_0 1.2.0 + hdy_tab_button_get_type@LIBHANDY_1_0 1.4.0 + hdy_tab_button_get_view@LIBHANDY_1_0 1.4.0 + hdy_tab_button_new@LIBHANDY_1_0 1.4.0 + hdy_tab_button_set_view@LIBHANDY_1_0 1.4.0 hdy_tab_page_get_child@LIBHANDY_1_0 1.2.0 hdy_tab_page_get_icon@LIBHANDY_1_0 1.2.0 hdy_tab_page_get_loading@LIBHANDY_1_0 1.2.0 @@ -375,6 +379,13 @@ libhandy-1.so.0 libhandy-1-0 #MINVER# hdy_tab_page_set_indicator_icon@LIBHANDY_1_0 1.2.0 hdy_tab_page_set_title@LIBHANDY_1_0 1.2.0 hdy_tab_page_set_tooltip@LIBHANDY_1_0 1.2.0 + hdy_tab_switcher_close@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_get_narrow@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_get_type@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_get_view@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_open@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_new@LIBHANDY_1_0 1.4.0 + hdy_tab_switcher_set_view@LIBHANDY_1_0 1.4.0 hdy_tab_view_add_page@LIBHANDY_1_0 1.2.0 hdy_tab_view_append@LIBHANDY_1_0 1.2.0 hdy_tab_view_append_pinned@LIBHANDY_1_0 1.2.0 diff --git a/doc/handy-docs.xml b/doc/handy-docs.xml index bd8cba416b65013a77bf7a4bd09da67b1ad95720..b256f9fc827c260db559c0c19ae4f57738b40763 100644 --- a/doc/handy-docs.xml +++ b/doc/handy-docs.xml @@ -64,6 +64,8 @@ + + @@ -141,6 +143,11 @@ + + Index of new symbols in 1.4 + + + Annotations glossary diff --git a/doc/meson.build b/doc/meson.build index e8dcf1a661234f98e07de6758726498662ee6a87..4f9bfefeabf0b0cbee6c4fd9c006d72ea9c93585 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -21,10 +21,12 @@ private_headers = [ 'hdy-preferences-page-private.h', 'hdy-shadow-helper-private.h', 'hdy-stackable-box-private.h', + 'hdy-swipe-away-bin-private.h', 'hdy-swipe-tracker-private.h', 'hdy-tab-private.h', 'hdy-tab-bar-private.h', 'hdy-tab-box-private.h', + 'hdy-tab-switcher-row-private.h', 'hdy-tab-view-private.h', 'hdy-types.h', 'hdy-view-switcher-button-private.h', diff --git a/examples/hdy-tab-view-demo-window.c b/examples/hdy-tab-view-demo-window.c index 7f4f46e38b67b88611bf7a63465b973c2654cac7..fe59cc9f06bea5364a8253b18ee630c7f2619a80 100644 --- a/examples/hdy-tab-view-demo-window.c +++ b/examples/hdy-tab-view-demo-window.c @@ -7,6 +7,7 @@ struct _HdyTabViewDemoWindow HdyWindow parent_instance; HdyTabView *view; HdyTabBar *tab_bar; + HdyTabSwitcher *tab_switcher; GActionMap *tab_action_group; @@ -117,6 +118,16 @@ tab_new (GSimpleAction *action, next_page++; } +static void +tab_switcher (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + HdyTabViewDemoWindow *self = HDY_TAB_VIEW_DEMO_WINDOW (user_data); + + hdy_tab_switcher_open (self->tab_switcher); +} + static HdyTabPage * get_current_page (HdyTabViewDemoWindow *self) { @@ -315,6 +326,7 @@ tab_duplicate (GSimpleAction *action, static GActionEntry action_entries[] = { { "window-new", window_new }, { "tab-new", tab_new }, + { "tab-switcher", tab_switcher }, }; static GActionEntry tab_action_entries[] = { @@ -462,6 +474,12 @@ extra_drag_data_received_cb (HdyTabViewDemoWindow *self, hdy_tab_page_set_title (page, text); } +static void +new_tab_cb (HdyTabViewDemoWindow *self) +{ + tab_new (NULL, NULL, self); +} + static void hdy_tab_view_demo_window_class_init (HdyTabViewDemoWindowClass *klass) { @@ -470,11 +488,13 @@ hdy_tab_view_demo_window_class_init (HdyTabViewDemoWindowClass *klass) gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-tab-view-demo-window.ui"); gtk_widget_class_bind_template_child (widget_class, HdyTabViewDemoWindow, view); gtk_widget_class_bind_template_child (widget_class, HdyTabViewDemoWindow, tab_bar); + gtk_widget_class_bind_template_child (widget_class, HdyTabViewDemoWindow, tab_switcher); gtk_widget_class_bind_template_callback (widget_class, page_detached_cb); gtk_widget_class_bind_template_callback (widget_class, setup_menu_cb); gtk_widget_class_bind_template_callback (widget_class, create_window_cb); gtk_widget_class_bind_template_callback (widget_class, indicator_activated_cb); gtk_widget_class_bind_template_callback (widget_class, extra_drag_data_received_cb); + gtk_widget_class_bind_template_callback (widget_class, new_tab_cb); } static void diff --git a/examples/hdy-tab-view-demo-window.ui b/examples/hdy-tab-view-demo-window.ui index e6a5aece824177e2d749eeeb59d4b5db1f1693f8..61013a8dde0c80c76c04519dcc46bd8947ee85b8 100644 --- a/examples/hdy-tab-view-demo-window.ui +++ b/examples/hdy-tab-view-demo-window.ui @@ -8,61 +8,85 @@ 800 600 - + True - False - vertical + view + 360 + - + True - True - + False + vertical - + True - win.tab-new + True + - + + + win.tab-new + + + True + tab-new-symbolic + + + + + + True - tab-new-symbolic + primary_menu + + + True + open-menu-symbolic + + + + end + + + + + + view + win.tab-switcher + + + end + - + True - primary_menu + - + True - open-menu-symbolic + view + - - end - - - - - - True - view - - - - - - True - True - tab_menu - HdyTabViewDemoWindow - - - - + + + True + True + tab_menu + HdyTabViewDemoWindow + + + + + + diff --git a/src/handy.gresources.xml b/src/handy.gresources.xml index b592baeaf9ce5bd4b0b508fdc48d615f8b115932..b8ef2a2d3f25a8d681c8ba4d432eb6560710c9b8 100644 --- a/src/handy.gresources.xml +++ b/src/handy.gresources.xml @@ -2,10 +2,14 @@ icons/scalable/actions/hdy-expander-arrow-symbolic.svg + icons/scalable/actions/hdy-list-add-large-symbolic.svg + icons/scalable/actions/hdy-sheet-collapse-symbolic.svg icons/scalable/actions/object-select-symbolic.svg icons/scalable/actions/pan-down-symbolic.svg icons/scalable/status/avatar-default-symbolic.svg + icons/scalable/status/hdy-tab-counter-symbolic.svg icons/scalable/status/hdy-tab-icon-missing-symbolic.svg + icons/scalable/status/hdy-tab-overflow-symbolic.svg themes/Adwaita.css themes/Adwaita-dark.css themes/fallback.css @@ -27,6 +31,9 @@ hdy-status-page.ui hdy-tab.ui hdy-tab-bar.ui + hdy-tab-button.ui + hdy-tab-switcher.ui + hdy-tab-switcher-row.ui hdy-view-switcher-bar.ui hdy-view-switcher-button.ui hdy-view-switcher-title.ui diff --git a/src/handy.h b/src/handy.h index 07257f82e70526920aedc59039b6b92983216169..d33250b6ad7d3b71d571379da2e188c01d91b34c 100644 --- a/src/handy.h +++ b/src/handy.h @@ -51,6 +51,8 @@ G_BEGIN_DECLS #include "hdy-swipe-tracker.h" #include "hdy-swipeable.h" #include "hdy-tab-bar.h" +#include "hdy-tab-button.h" +#include "hdy-tab-switcher.h" #include "hdy-tab-view.h" #include "hdy-title-bar.h" #include "hdy-types.h" diff --git a/src/hdy-flap.c b/src/hdy-flap.c index 36e5c59c1bf2b3b0a9585a6d570a161ef2e8bdde..3ae809b02e4cef5ab2c3e677403bcf38f7bbadf7 100644 --- a/src/hdy-flap.c +++ b/src/hdy-flap.c @@ -355,6 +355,19 @@ set_reveal_flap (HdyFlap *self, hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self), reveal_flap ? 1 : 0, duration); } + if (self->reveal_flap && + self->content.widget && + self->flap.widget && + self->modal && + self->fold_progress > 0 && + gtk_widget_get_mapped (GTK_WIDGET (self))) { + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + GtkWidget *focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); + + if (gtk_widget_is_ancestor (focus, self->content.widget)) + gtk_widget_child_focus (GTK_WIDGET (self), GTK_DIR_TAB_FORWARD); + } + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_FLAP]); } @@ -984,6 +997,7 @@ hdy_flap_size_allocate (GtkWidget *widget, self->fold_progress > 0); gtk_widget_set_clip (widget, alloc); + gtk_widget_queue_draw (widget); } /* This private method is prefixed by the call name because it will be a virtual @@ -1264,6 +1278,30 @@ hdy_flap_direction_changed (GtkWidget *widget, previous_direction); } +static gboolean +hdy_flap_focus (GtkWidget *widget, + GtkDirectionType direction) +{ + HdyFlap *self = HDY_FLAP (widget); + + if (!gtk_widget_get_can_focus (widget) && + self->content.widget && + self->flap.widget && + self->modal && + self->reveal_progress > 0 && + self->fold_progress > 0) { + if (gtk_widget_child_focus (GTK_WIDGET (self->flap.widget), direction)) + return GDK_EVENT_STOP; + + if (self->separator.widget) + return gtk_widget_child_focus (GTK_WIDGET (self->separator.widget), direction); + + return GDK_EVENT_PROPAGATE; + } + + return GTK_WIDGET_CLASS (hdy_flap_parent_class)->focus (widget, direction); +} + static void hdy_flap_forall (GtkContainer *container, gboolean include_internals, @@ -1470,6 +1508,7 @@ hdy_flap_class_init (HdyFlapClass *klass) widget_class->realize = hdy_flap_realize; widget_class->unrealize = hdy_flap_unrealize; widget_class->direction_changed = hdy_flap_direction_changed; + widget_class->focus = hdy_flap_focus; container_class->remove = hdy_flap_remove; container_class->add = hdy_flap_add; diff --git a/src/hdy-swipe-away-bin-private.h b/src/hdy-swipe-away-bin-private.h new file mode 100644 index 0000000000000000000000000000000000000000..60901945f6bdb37c6dac8313863e335f191e9006 --- /dev/null +++ b/src/hdy-swipe-away-bin-private.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020-2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only can be included directly." +#endif + +#include "hdy-version.h" + +#include + +G_BEGIN_DECLS + +#define HDY_TYPE_SWIPE_AWAY_BIN (hdy_swipe_away_bin_get_type()) + +G_DECLARE_FINAL_TYPE (HdySwipeAwayBin, hdy_swipe_away_bin, HDY, SWIPE_AWAY_BIN, GtkEventBox) + +void hdy_swipe_away_bin_cancel (HdySwipeAwayBin *self); + +G_END_DECLS diff --git a/src/hdy-swipe-away-bin.c b/src/hdy-swipe-away-bin.c new file mode 100644 index 0000000000000000000000000000000000000000..43da3f5310177e2a6fac37d5e033e6dbcb46e324 --- /dev/null +++ b/src/hdy-swipe-away-bin.c @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2020-2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ + +#include "config.h" +#include + +#include "hdy-swipe-away-bin-private.h" + +#include "hdy-animation-private.h" +#include "hdy-swipeable.h" +#include "hdy-swipe-tracker.h" + +struct _HdySwipeAwayBin +{ + GtkEventBox parent_instance; + + gdouble progress; + HdySwipeTracker *tracker; + HdyAnimation *animation; +}; + +enum { + PROP_0, + PROP_ENABLED, + LAST_PROP +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_REMOVED, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void hdy_swipe_away_bin_swipeable_init (HdySwipeableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdySwipeAwayBin, hdy_swipe_away_bin, GTK_TYPE_EVENT_BOX, + G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_swipe_away_bin_swipeable_init)) + +static void +set_progress (HdySwipeAwayBin *self, + gdouble progress) +{ + self->progress = progress; + + gtk_widget_set_opacity (GTK_WIDGET (self), hdy_ease_out_cubic (1 - ABS (self->progress))); + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +static void +animation_value_cb (gdouble value, + HdySwipeAwayBin *self) +{ + set_progress (self, value); +} + +static gboolean +animation_done_idle_cb (HdySwipeAwayBin *self) +{ + g_signal_emit (self, signals[SIGNAL_REMOVED], 0); + + return G_SOURCE_REMOVE; +} + +static void +animation_done_cb (HdySwipeAwayBin *self) +{ + g_clear_pointer (&self->animation, hdy_animation_unref); + + if (ABS (self->progress) < 1) + return; + + g_idle_add ((GSourceFunc) animation_done_idle_cb, self); +} + +static void +animate (HdySwipeAwayBin *self, + gint64 duration, + gdouble to) +{ + self->animation = + hdy_animation_new (GTK_WIDGET (self), self->progress, to, duration, + hdy_ease_out_cubic, + (HdyAnimationValueCallback) animation_value_cb, + (HdyAnimationDoneCallback) animation_done_cb, + self); + + hdy_animation_start (self->animation); +} + +static void +begin_swipe_cb (HdySwipeAwayBin *self) +{ + if (self->animation) + hdy_animation_stop (self->animation); +} + +static void +update_swipe_cb (HdySwipeAwayBin *self, + gdouble progress) +{ + set_progress (self, progress); +} + +static void +end_swipe_cb (HdySwipeAwayBin *self, + gint64 duration, + gdouble to) +{ + animate (self, duration, to); +} + +static void +hdy_swipe_away_bin_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (widget); + GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget)); + GtkAllocation child_alloc; + + GTK_WIDGET_CLASS (hdy_swipe_away_bin_parent_class)->size_allocate (widget, alloc); + + if (!child || !gtk_widget_get_visible (child)) + return; + + child_alloc.x = alloc->x - (gint) (self->progress * alloc->width); + child_alloc.y = alloc->y; + child_alloc.width = alloc->width; + child_alloc.height = alloc->height; + + gtk_widget_size_allocate (child, &child_alloc); +} + +static void +hdy_swipe_away_bin_dispose (GObject *object) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (object); + + g_clear_object (&self->tracker); + + G_OBJECT_CLASS (hdy_swipe_away_bin_parent_class)->dispose (object); +} + +static void +hdy_swipe_away_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (object); + + switch (prop_id) { + case PROP_ENABLED: + g_value_set_boolean (value, hdy_swipe_tracker_get_enabled (self->tracker)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_swipe_away_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (object); + + switch (prop_id) { + case PROP_ENABLED: + hdy_swipe_tracker_set_enabled (self->tracker, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_swipe_away_bin_class_init (HdySwipeAwayBinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = hdy_swipe_away_bin_dispose; + object_class->get_property = hdy_swipe_away_bin_get_property; + object_class->set_property = hdy_swipe_away_bin_set_property; + + widget_class->size_allocate = hdy_swipe_away_bin_size_allocate; + + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + _("Enabled"), + _("Enabled"), + TRUE, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[SIGNAL_REMOVED] = + g_signal_new ("removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +hdy_swipe_away_bin_init (HdySwipeAwayBin *self) +{ + self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self)); + + g_signal_connect_swapped (self->tracker, "begin-swipe", + G_CALLBACK (begin_swipe_cb), self); + g_signal_connect_swapped (self->tracker, "update-swipe", + G_CALLBACK (update_swipe_cb), self); + g_signal_connect_swapped (self->tracker, "end-swipe", + G_CALLBACK (end_swipe_cb), self); +} + +static HdySwipeTracker * +hdy_swipe_away_bin_get_swipe_tracker (HdySwipeable *swipeable) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (swipeable); + + return self->tracker; +} + +static gdouble +hdy_swipe_away_bin_get_distance (HdySwipeable *swipeable) +{ + return (gdouble) gtk_widget_get_allocated_width (GTK_WIDGET (swipeable)); +} + +static gdouble +hdy_swipe_away_bin_get_cancel_progress (HdySwipeable *swipeable) +{ + return 0; +} + +static gdouble +hdy_swipe_away_bin_get_progress (HdySwipeable *swipeable) +{ + HdySwipeAwayBin *self = HDY_SWIPE_AWAY_BIN (swipeable); + + return self->progress; +} + +static gdouble * +hdy_swipe_away_bin_get_snap_points (HdySwipeable *swipeable, + gint *n_snap_points) +{ + gdouble *points = g_new0 (gdouble, 3); + + points[0] = -1; + points[2] = 1; + + if (n_snap_points) + *n_snap_points = 3; + + return points; +} + +static void +hdy_swipe_away_bin_switch_child (HdySwipeable *swipeable, + guint index, + gint64 duration) +{ +} + +static void +hdy_swipe_away_bin_swipeable_init (HdySwipeableInterface *iface) +{ + iface->get_swipe_tracker = hdy_swipe_away_bin_get_swipe_tracker; + iface->get_distance = hdy_swipe_away_bin_get_distance; + iface->get_cancel_progress = hdy_swipe_away_bin_get_cancel_progress; + iface->get_progress = hdy_swipe_away_bin_get_progress; + iface->get_snap_points = hdy_swipe_away_bin_get_snap_points; + iface->switch_child = hdy_swipe_away_bin_switch_child; +} + +void +hdy_swipe_away_bin_cancel (HdySwipeAwayBin *self) +{ + g_return_if_fail (HDY_IS_SWIPE_AWAY_BIN (self)); + + if (self->animation) + hdy_animation_stop (self->animation); + + animate (self, 200, 0); +} diff --git a/src/hdy-tab-button.c b/src/hdy-tab-button.c new file mode 100644 index 0000000000000000000000000000000000000000..2fb6a1580f92ab79c828a33187c9a8637de98b57 --- /dev/null +++ b/src/hdy-tab-button.c @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#include "config.h" +#include + +#include "hdy-tab-button.h" + +/* Copied from GtkInspector code */ +#define XFT_DPI_MULTIPLIER (96.0 * PANGO_SCALE) + +/** + * SECTION:hdy-tab-button + * @short_description: A button that displays the number of #HdyTabView pages + * @title: HdyTabButton + * @See_also: #HdyTabView + * + * The #HdyTabButton widget is a #GtkButton subclass that displays the number + * of pages in a given #HdyTabView. + * + * It can be used to open a tab switcher view in a mobile UI. + * + * # CSS nodes + * + * #HdyTabButton has a main CSS node with name button and style class + * .tab-button. + * + * It contains the subnode overlay, which contains nodes image and label. The + * label subnode can contain the .small style class for 10 or more tabs. + * + * Since: 1.4 + */ + +struct _HdyTabButton +{ + GtkButton parent_instance; + + GtkLabel *label; + GtkImage *icon; + + HdyTabView *view; +}; + +G_DEFINE_TYPE (HdyTabButton, hdy_tab_button, GTK_TYPE_BUTTON) + +enum { + PROP_0, + PROP_VIEW, + LAST_PROP +}; + +static GParamSpec *props[LAST_PROP]; + +/* FIXME: I hope there is a better way to prevent the label from changing scale */ +static void +update_label_scale (HdyTabButton *self, + GtkSettings *settings) +{ + gint xft_dpi; + PangoAttrList *attrs; + PangoAttribute *scale_attribute; + + g_object_get (settings, "gtk-xft-dpi", &xft_dpi, NULL); + + attrs = pango_attr_list_new (); + + scale_attribute = pango_attr_scale_new (XFT_DPI_MULTIPLIER / (gdouble) xft_dpi); + + pango_attr_list_change (attrs, scale_attribute); + + gtk_label_set_attributes (self->label, attrs); + + pango_attr_list_unref (attrs); +} + +static void +xft_dpi_changed (HdyTabButton *self, + GParamSpec *pspec, + GtkSettings *settings) +{ + update_label_scale (self, settings); +} + +static void +update_icon (HdyTabButton *self) +{ + gboolean display_label = FALSE; + gboolean small_label = FALSE; + const gchar *icon_name = "hdy-tab-counter-symbolic"; + g_autofree gchar *label_text = NULL; + GtkStyleContext *context; + + if (self->view) { + guint n_pages = hdy_tab_view_get_n_pages (self->view); + + small_label = n_pages >= 10; + + if (n_pages < 100) { + display_label = TRUE; + label_text = g_strdup_printf ("%u", n_pages); + } else { + icon_name = "hdy-tab-overflow-symbolic"; + } + } + + context = gtk_widget_get_style_context (GTK_WIDGET (self->label)); + + if (small_label) + gtk_style_context_add_class (context, "small"); + else + gtk_style_context_remove_class (context, "small"); + + gtk_widget_set_visible (GTK_WIDGET (self->label), display_label); + gtk_label_set_text (self->label, label_text); + gtk_image_set_from_icon_name (self->icon, icon_name, GTK_ICON_SIZE_BUTTON); +} + +static void +hdy_tab_button_dispose (GObject *object) +{ + HdyTabButton *self = HDY_TAB_BUTTON (object); + + hdy_tab_button_set_view (self, NULL); + + G_OBJECT_CLASS (hdy_tab_button_parent_class)->dispose (object); +} + +static void +hdy_tab_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyTabButton *self = HDY_TAB_BUTTON (object); + + switch (prop_id) { + case PROP_VIEW: + g_value_set_object (value, hdy_tab_button_get_view (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyTabButton *self = HDY_TAB_BUTTON (object); + + switch (prop_id) { + case PROP_VIEW: + hdy_tab_button_set_view (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_button_class_init (HdyTabButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = hdy_tab_button_dispose; + object_class->get_property = hdy_tab_button_get_property; + object_class->set_property = hdy_tab_button_set_property; + + /** + * HdyTabButton:view: + * + * The #HdyTabView the tab button displays. + * + * Since: 1.4 + */ + props[PROP_VIEW] = + g_param_spec_object ("view", + _("View"), + _("The view the tab button displays."), + HDY_TYPE_TAB_VIEW, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-tab-button.ui"); + + gtk_widget_class_bind_template_child (widget_class, HdyTabButton, label); + gtk_widget_class_bind_template_child (widget_class, HdyTabButton, icon); +} + +static void +hdy_tab_button_init (HdyTabButton *self) +{ + GtkSettings *settings; + + gtk_widget_init_template (GTK_WIDGET (self)); + + update_icon (self); + + settings = gtk_widget_get_settings (GTK_WIDGET (self)); + + update_label_scale (self, settings); + g_signal_connect_object (settings, "notify::gtk-xft-dpi", + G_CALLBACK (xft_dpi_changed), self, + G_CONNECT_SWAPPED); +} + +/** + * hdy_tab_button_new: + * + * Creates a new #HdyTabButton widget. + * + * Returns: a new #HdyTabButton + * + * Since: 1.4 + */ +GtkWidget * +hdy_tab_button_new (void) +{ + return g_object_new (HDY_TYPE_TAB_BUTTON, NULL); +} + +/** + * hdy_tab_button_get_view: + * @self: a #HdyTabButton + * + * Gets the #HdyTabView @self displays. + * + * Returns: (transfer none) (nullable): the #HdyTabView @self displays + * + * Since: 1.4 + */ +HdyTabView * +hdy_tab_button_get_view (HdyTabButton *self) +{ + g_return_val_if_fail (HDY_IS_TAB_BUTTON (self), NULL); + + return self->view; +} + +/** + * hdy_tab_button_set_view: + * @self: a #HdyTabButton + * @view: (nullable): a #HdyTabView + * + * Sets the #HdyTabView @self displays. + * + * Since: 1.4 + */ +void +hdy_tab_button_set_view (HdyTabButton *self, + HdyTabView *view) +{ + g_return_if_fail (HDY_IS_TAB_BUTTON (self)); + g_return_if_fail (HDY_IS_TAB_VIEW (view) || view == NULL); + + if (self->view == view) + return; + + if (self->view) + g_signal_handlers_disconnect_by_func (self->view, + update_icon, + self); + + g_set_object (&self->view, view); + + if (self->view) + g_signal_connect_object (self->view, "notify::n-pages", + G_CALLBACK (update_icon), self, + G_CONNECT_SWAPPED); + + update_icon (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]); +} diff --git a/src/hdy-tab-button.h b/src/hdy-tab-button.h new file mode 100644 index 0000000000000000000000000000000000000000..91c76227de72c405954524ab34627017a3ce4727 --- /dev/null +++ b/src/hdy-tab-button.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only can be included directly." +#endif + +#include "hdy-version.h" + +#include +#include "hdy-tab-view.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_TAB_BUTTON (hdy_tab_button_get_type()) + +HDY_AVAILABLE_IN_1_4 +G_DECLARE_FINAL_TYPE (HdyTabButton, hdy_tab_button, HDY, TAB_BUTTON, GtkButton) + +HDY_AVAILABLE_IN_1_4 +GtkWidget *hdy_tab_button_new (void); + +HDY_AVAILABLE_IN_1_4 +HdyTabView *hdy_tab_button_get_view (HdyTabButton *self); +HDY_AVAILABLE_IN_1_4 +void hdy_tab_button_set_view (HdyTabButton *self, + HdyTabView *view); + +G_END_DECLS diff --git a/src/hdy-tab-button.ui b/src/hdy-tab-button.ui new file mode 100644 index 0000000000000000000000000000000000000000..22b6beaafc91627977ee75fd0eac1efab7c6bbbe --- /dev/null +++ b/src/hdy-tab-button.ui @@ -0,0 +1,34 @@ + + + + + diff --git a/src/hdy-tab-switcher-row-private.h b/src/hdy-tab-switcher-row-private.h new file mode 100644 index 0000000000000000000000000000000000000000..7f3dabb996a74a292b972039fb195dbc1776c01a --- /dev/null +++ b/src/hdy-tab-switcher-row-private.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only can be included directly." +#endif + +#include "hdy-version.h" + +#include +#include "hdy-tab-view.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_TAB_SWITCHER_ROW (hdy_tab_switcher_row_get_type()) + +G_DECLARE_FINAL_TYPE (HdyTabSwitcherRow, hdy_tab_switcher_row, HDY, TAB_SWITCHER_ROW, GtkListBoxRow) + +GtkWidget *hdy_tab_switcher_row_new (HdyTabPage *page, + HdyTabView *view); + +HdyTabPage *hdy_tab_switcher_row_get_page (HdyTabSwitcherRow *self); + +gboolean hdy_tab_switcher_row_is_animating (HdyTabSwitcherRow *self); + +void hdy_tab_switcher_row_animate_open (HdyTabSwitcherRow *self); +void hdy_tab_switcher_row_animate_close (HdyTabSwitcherRow *self); + +G_END_DECLS diff --git a/src/hdy-tab-switcher-row.c b/src/hdy-tab-switcher-row.c new file mode 100644 index 0000000000000000000000000000000000000000..ec4e0c1c80e43e67b58e222839e0c857a798dce5 --- /dev/null +++ b/src/hdy-tab-switcher-row.c @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#include "config.h" +#include + +#include "hdy-tab-switcher-row-private.h" + +#include "hdy-fading-label-private.h" +#include "hdy-swipe-away-bin-private.h" + +struct _HdyTabSwitcherRow +{ + GtkListBoxRow parent_instance; + + GtkRevealer *revealer; + HdySwipeAwayBin *swipe_bin; + GtkStack *icon_stack; + GtkImage *icon; + GtkSpinner *spinner; + HdyFadingLabel *title; + GtkWidget *indicator_btn; + GtkImage *indicator_icon; + GtkWidget *close_btn; + + HdyTabPage *page; + HdyTabView *view; + + guint remove_timeout; +}; + +G_DEFINE_TYPE (HdyTabSwitcherRow, hdy_tab_switcher_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + PROP_0, + PROP_PAGE, + PROP_VIEW, + LAST_PROP +}; + +static GParamSpec *props[LAST_PROP]; + +static inline void +set_style_class (GtkWidget *widget, + const gchar *style_class, + gboolean enabled) +{ + GtkStyleContext *context = gtk_widget_get_style_context (widget); + + if (enabled) + gtk_style_context_add_class (context, style_class); + else + gtk_style_context_remove_class (context, style_class); +} + +static void +update_pinned (HdyTabSwitcherRow *self) +{ + set_style_class (GTK_WIDGET (self), "pinned", + hdy_tab_page_get_pinned (self->page)); +} + +static void +update_icon (HdyTabSwitcherRow *self) +{ + GIcon *gicon = hdy_tab_page_get_icon (self->page); + gboolean loading = hdy_tab_page_get_loading (self->page); + const gchar *name = loading ? "spinner" : "icon"; + + if (!gicon) + gicon = hdy_tab_view_get_default_icon (self->view); + + gtk_image_set_from_gicon (self->icon, gicon, GTK_ICON_SIZE_BUTTON); + gtk_stack_set_visible_child_name (self->icon_stack, name); +} + +static void +update_spinner (HdyTabSwitcherRow *self) +{ + gboolean loading = self->page && hdy_tab_page_get_loading (self->page); + gboolean mapped = gtk_widget_get_mapped (GTK_WIDGET (self)); + + /* Don't use CPU when not needed */ + if (loading && mapped) + gtk_spinner_start (self->spinner); + else if (gtk_widget_get_realized (GTK_WIDGET (self))) + gtk_spinner_stop (self->spinner); +} + +static void +update_loading (HdyTabSwitcherRow *self) +{ + update_icon (self); + update_spinner (self); + set_style_class (GTK_WIDGET (self), "loading", + hdy_tab_page_get_loading (self->page)); +} + +static void +update_indicator (HdyTabSwitcherRow *self) +{ + GIcon *indicator = hdy_tab_page_get_indicator_icon (self->page); + gboolean activatable = hdy_tab_page_get_indicator_activatable (self->page); + + gtk_image_set_from_gicon (self->indicator_icon, indicator, GTK_ICON_SIZE_BUTTON); + gtk_widget_set_visible (GTK_WIDGET (self->indicator_btn), indicator != NULL); + gtk_widget_set_sensitive (GTK_WIDGET (self->indicator_btn), activatable); +} + +static void +update_needs_attention (HdyTabSwitcherRow *self) +{ + set_style_class (GTK_WIDGET (self), "needs-attention", + hdy_tab_page_get_needs_attention (self->page)); +} + +static void +indicator_clicked_cb (HdyTabSwitcherRow *self) +{ + if (!self->page) + return; + + g_signal_emit_by_name (self->view, "indicator-activated", self->page); +} + +static void +close_clicked_cb (HdyTabSwitcherRow *self) +{ + if (!self->page) + return; + + hdy_tab_view_close_page (self->view, self->page); +} + +static gboolean +removed_timeout_cb (HdyTabSwitcherRow *self) +{ + if (self->page) + hdy_swipe_away_bin_cancel (self->swipe_bin); + + return G_SOURCE_REMOVE; +} + +static void +removed_cb (HdyTabSwitcherRow *self) +{ + if (!self->page) + return; + + hdy_tab_view_close_page (self->view, self->page); + + self->remove_timeout = g_timeout_add_seconds (1, (GSourceFunc) removed_timeout_cb, self); +} + +static void +hdy_tab_switcher_row_map (GtkWidget *widget) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (widget); + + GTK_WIDGET_CLASS (hdy_tab_switcher_row_parent_class)->map (widget); + + update_spinner (self); +} + +static void +hdy_tab_switcher_row_unmap (GtkWidget *widget) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (widget); + + GTK_WIDGET_CLASS (hdy_tab_switcher_row_parent_class)->unmap (widget); + + update_spinner (self); +} + +static void +hdy_tab_switcher_row_constructed (GObject *object) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (object); + + G_OBJECT_CLASS (hdy_tab_switcher_row_parent_class)->constructed (object); + + g_object_bind_property (self->page, "title", + self->title, "label", + G_BINDING_SYNC_CREATE); + g_object_bind_property (self->page, "pinned", + self->swipe_bin, "enabled", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + g_object_bind_property (self->page, "pinned", + self->close_btn, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + g_signal_connect_object (self->page, "notify::pinned", + G_CALLBACK (update_pinned), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->page, "notify::icon", + G_CALLBACK (update_icon), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->page, "notify::loading", + G_CALLBACK (update_loading), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->page, "notify::indicator-icon", + G_CALLBACK (update_indicator), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->page, "notify::indicator-activatable", + G_CALLBACK (update_indicator), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->page, "notify::needs-attention", + G_CALLBACK (update_needs_attention), self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->view, "notify::default-icon", + G_CALLBACK (update_icon), self, + G_CONNECT_SWAPPED); + + update_pinned (self); + update_loading (self); + update_indicator (self); + update_needs_attention (self); +} + +static void +hdy_tab_switcher_row_dispose (GObject *object) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (object); + + if (self->remove_timeout) { + g_source_remove (self->remove_timeout); + self->remove_timeout = 0; + } + + G_OBJECT_CLASS (hdy_tab_switcher_row_parent_class)->dispose (object); +} + +static void +hdy_tab_switcher_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (object); + + switch (prop_id) { + case PROP_PAGE: + g_value_set_object (value, self->page); + break; + case PROP_VIEW: + g_value_set_object (value, self->view); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_switcher_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyTabSwitcherRow *self = HDY_TAB_SWITCHER_ROW (object); + + switch (prop_id) { + case PROP_PAGE: + self->page = g_value_get_object (value); + break; + case PROP_VIEW: + self->view = g_value_get_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_switcher_row_class_init (HdyTabSwitcherRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = hdy_tab_switcher_row_constructed; + object_class->dispose = hdy_tab_switcher_row_dispose; + object_class->get_property = hdy_tab_switcher_row_get_property; + object_class->set_property = hdy_tab_switcher_row_set_property; + + widget_class->map = hdy_tab_switcher_row_map; + widget_class->unmap = hdy_tab_switcher_row_unmap; + + props[PROP_PAGE] = + g_param_spec_object ("page", + _("Page"), + _("The page the row displays."), + HDY_TYPE_TAB_PAGE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_VIEW] = + g_param_spec_object ("view", + _("View"), + _("The view containing the page."), + HDY_TYPE_TAB_VIEW, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-tab-switcher-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, revealer); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, swipe_bin); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, icon_stack); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, icon); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, spinner); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, title); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, indicator_btn); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, indicator_icon); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcherRow, close_btn); + + gtk_widget_class_bind_template_callback (widget_class, indicator_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, close_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, removed_cb); +} + +static void +hdy_tab_switcher_row_init (HdyTabSwitcherRow *self) +{ + g_type_ensure (HDY_TYPE_FADING_LABEL); + g_type_ensure (HDY_TYPE_SWIPE_AWAY_BIN); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +hdy_tab_switcher_row_new (HdyTabPage *page, + HdyTabView *view) +{ + g_return_val_if_fail (HDY_IS_TAB_PAGE (page), NULL); + g_return_val_if_fail (HDY_IS_TAB_VIEW (view), NULL); + + return g_object_new (HDY_TYPE_TAB_SWITCHER_ROW, + "page", page, + "view", view, + NULL); +} + +HdyTabPage * +hdy_tab_switcher_row_get_page (HdyTabSwitcherRow *self) +{ + g_return_val_if_fail (HDY_IS_TAB_SWITCHER_ROW (self), NULL); + + return self->page; +} + +void +hdy_tab_switcher_row_animate_close (HdyTabSwitcherRow *self) +{ + g_return_if_fail (HDY_IS_TAB_SWITCHER_ROW (self)); + + if (!self->page) + return; + + g_signal_handlers_disconnect_by_func (self->view, G_CALLBACK (update_icon), self); + + g_signal_handlers_disconnect_by_func (self->page, G_CALLBACK (update_pinned), self); + g_signal_handlers_disconnect_by_func (self->page, G_CALLBACK (update_icon), self); + g_signal_handlers_disconnect_by_func (self->page, G_CALLBACK (update_loading), self); + g_signal_handlers_disconnect_by_func (self->page, G_CALLBACK (update_indicator), self); + g_signal_handlers_disconnect_by_func (self->page, G_CALLBACK (update_needs_attention), self); + + if (self->remove_timeout) { + g_source_remove (self->remove_timeout); + self->remove_timeout = 0; + } + + self->page = NULL; + + g_signal_connect_swapped (self->revealer, "notify::child-revealed", + G_CALLBACK (gtk_widget_destroy), self); + + gtk_revealer_set_reveal_child (self->revealer, FALSE); +} + +void +hdy_tab_switcher_row_animate_open (HdyTabSwitcherRow *self) +{ + g_return_if_fail (HDY_IS_TAB_SWITCHER_ROW (self)); + + if (!self->page) + return; + + gtk_widget_show (GTK_WIDGET (self)); + gtk_revealer_set_reveal_child (self->revealer, TRUE); +} + +gboolean +hdy_tab_switcher_row_is_animating (HdyTabSwitcherRow *self) +{ + g_return_val_if_fail (HDY_IS_TAB_SWITCHER_ROW (self), FALSE); + + return self->page == NULL; +} diff --git a/src/hdy-tab-switcher-row.ui b/src/hdy-tab-switcher-row.ui new file mode 100644 index 0000000000000000000000000000000000000000..9ff3c948158b4d9b90cb0ac650860a8251f7aae0 --- /dev/null +++ b/src/hdy-tab-switcher-row.ui @@ -0,0 +1,96 @@ + + + + + diff --git a/src/hdy-tab-switcher.c b/src/hdy-tab-switcher.c new file mode 100644 index 0000000000000000000000000000000000000000..1e034fa4e8b1080ac85f61c6bea837243c574f98 --- /dev/null +++ b/src/hdy-tab-switcher.c @@ -0,0 +1,681 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#include "config.h" +#include + +#include "hdy-tab-switcher.h" + +#include "hdy-flap.h" +#include "hdy-tab-switcher-row-private.h" + +/** + * SECTION:hdy-tab-switcher + * @short_description: TODO + * @title: HdyTabSwitcher + * @See_also: #HdyTabView + * + * The #HdyTabSwitcher widget is a TODO + * + * Since: 1.4 + */ + +struct _HdyTabSwitcher +{ + GtkBin parent_instance; + + HdyFlap *flap; + GtkListBox *list; + + GtkGesture *click_gesture; + GtkGesture *long_press_gesture; + GtkMenu *context_menu; + GtkPopover *touch_menu; + + HdyTabView *view; + gboolean narrow; +}; + +G_DEFINE_TYPE (HdyTabSwitcher, hdy_tab_switcher, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_VIEW, + PROP_NARROW, + LAST_PROP +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_NEW_TAB, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static gboolean +reset_setup_menu_cb (HdyTabSwitcher *self) +{ + g_signal_emit_by_name (self->view, "setup-menu", NULL); + + return G_SOURCE_REMOVE; +} + +static void +touch_menu_notify_visible_cb (HdyTabSwitcher *self) +{ + if (!self->touch_menu || gtk_widget_get_visible (GTK_WIDGET (self->touch_menu))) + return; + + g_idle_add ((GSourceFunc) reset_setup_menu_cb, self); +} + +static void +destroy_cb (HdyTabSwitcher *self) +{ + self->touch_menu = NULL; +} + +static void +do_touch_popup (HdyTabSwitcher *self, + HdyTabSwitcherRow *row) +{ + GMenuModel *model = hdy_tab_view_get_menu_model (self->view); + HdyTabPage *page = hdy_tab_switcher_row_get_page (row); + + if (!G_IS_MENU_MODEL (model)) + return; + + g_signal_emit_by_name (self->view, "setup-menu", page); + + if (!self->touch_menu) { + self->touch_menu = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (row), model)); + gtk_popover_set_constrain_to (self->touch_menu, GTK_POPOVER_CONSTRAINT_WINDOW); + + g_signal_connect_object (self->touch_menu, "notify::visible", + G_CALLBACK (touch_menu_notify_visible_cb), self, + G_CONNECT_AFTER | G_CONNECT_SWAPPED); + + g_signal_connect_object (self->touch_menu, "destroy", + G_CALLBACK (destroy_cb), self, + G_CONNECT_AFTER | G_CONNECT_SWAPPED); + } else + gtk_popover_set_relative_to (self->touch_menu, GTK_WIDGET (row)); + + gtk_popover_popup (self->touch_menu); +} + +static void +popup_menu_detach (HdyTabSwitcher *self, + GtkMenu *menu) +{ + self->context_menu = NULL; +} + +static void +popup_menu_deactivate_cb (HdyTabSwitcher *self) +{ + g_idle_add ((GSourceFunc) reset_setup_menu_cb, self); +} + +static void +do_popup (HdyTabSwitcher *self, + HdyTabSwitcherRow *row, + GdkEvent *event) +{ + GMenuModel *model = hdy_tab_view_get_menu_model (self->view); + HdyTabPage *page = hdy_tab_switcher_row_get_page (row); + + if (!G_IS_MENU_MODEL (model)) + return; + + g_signal_emit_by_name (self->view, "setup-menu", page); + + if (!self->context_menu) { + self->context_menu = GTK_MENU (gtk_menu_new_from_model (model)); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->context_menu)), + GTK_STYLE_CLASS_CONTEXT_MENU); + + g_signal_connect_object (self->context_menu, + "deactivate", + G_CALLBACK (popup_menu_deactivate_cb), + self, + G_CONNECT_SWAPPED); + + gtk_menu_attach_to_widget (self->context_menu, GTK_WIDGET (self), + (GtkMenuDetachFunc) popup_menu_detach); + } + + if (event && gdk_event_triggers_context_menu (event)) { + gtk_menu_popup_at_pointer (self->context_menu, event); + } else { + gtk_menu_popup_at_widget (self->context_menu, + GTK_WIDGET (row), + GDK_GRAVITY_SOUTH_WEST, + GDK_GRAVITY_NORTH_WEST, + event); + + gtk_menu_shell_select_first (GTK_MENU_SHELL (self->context_menu), FALSE); + } +} + +static void +reveal_flap_cb (HdyTabSwitcher *self) +{ + HdyTabPage *page; + GtkWidget *child; + + if (hdy_flap_get_reveal_flap (self->flap)) { + gtk_widget_grab_focus (GTK_WIDGET (self->list)); + } else { + page = hdy_tab_view_get_selected_page (self->view); + child = hdy_tab_page_get_child (page); + + gtk_widget_grab_focus (GTK_WIDGET (child)); + } +} + +static void +collapse_cb (HdyTabSwitcher *self) +{ + hdy_tab_switcher_close (self); +} + +static void +new_tab_cb (HdyTabSwitcher *self) +{ + g_signal_emit (self, signals[SIGNAL_NEW_TAB], 0); + + hdy_tab_switcher_close (self); +} + +static void +row_selected_cb (HdyTabSwitcher *self, + HdyTabSwitcherRow *row) +{ + HdyTabPage *page; + + if (!row) + return; + + g_assert (HDY_IS_TAB_SWITCHER_ROW (row)); + + if (!self->view) + return; + + page = hdy_tab_switcher_row_get_page (row); + hdy_tab_view_set_selected_page (self->view, page); +} + +static void +row_activated_cb (HdyTabSwitcher *self) +{ + hdy_tab_switcher_close (self); +} + +static HdyTabSwitcherRow * +find_nth_alive_row (HdyTabSwitcher *self, + guint position) +{ + GtkListBoxRow *row = NULL; + guint i = 0; + + do { + row = gtk_list_box_get_row_at_index (self->list, i++); + + if (hdy_tab_switcher_row_is_animating (HDY_TAB_SWITCHER_ROW (row))) + position++; + } while (i <= position); + + return HDY_TAB_SWITCHER_ROW (row); +} + +static void +pages_changed_cb (HdyTabSwitcher *self, + guint position, + guint removed, + guint added, + GListModel *pages) +{ + guint i; + + while (removed--) { + HdyTabSwitcherRow *row = find_nth_alive_row (self, position); + + hdy_tab_switcher_row_animate_close (HDY_TAB_SWITCHER_ROW (row)); + } + + for (i = 0; i < added; i++) { + g_autoptr (HdyTabPage) page = g_list_model_get_item (pages, position + i); + GtkWidget *row = hdy_tab_switcher_row_new (page, self->view); + + gtk_list_box_insert (self->list, row, position + i); + hdy_tab_switcher_row_animate_open (HDY_TAB_SWITCHER_ROW (row)); + } +} + +static void +notify_selected_page_cb (HdyTabSwitcher *self) +{ + HdyTabPage *page = NULL; + + if (self->view) + page = hdy_tab_view_get_selected_page (self->view); + + if (page) { + gint index = hdy_tab_view_get_page_position (self->view, page); + HdyTabSwitcherRow *row = find_nth_alive_row (self, index); + + gtk_list_box_select_row (self->list, GTK_LIST_BOX_ROW (row)); + } else { + gtk_list_box_unselect_all (self->list); + } +} + +static void +click_pressed_cb (HdyTabSwitcher *self, + gint n_press, + gdouble x, + gdouble y) +{ + g_autoptr (GdkEvent) event = NULL; + GtkListBoxRow *row; + HdyTabPage *page; + guint button; + + if (n_press > 1) { + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_DENIED); + + return; + } + + row = gtk_list_box_get_row_at_y (self->list, y); + + if (!row) { + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_DENIED); + + return; + } + + event = gtk_get_current_event (); + button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (self->click_gesture)); + page = hdy_tab_switcher_row_get_page (HDY_TAB_SWITCHER_ROW (row)); + + if (event && gdk_event_triggers_context_menu (event)) { + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_CLAIMED); + do_popup (self, HDY_TAB_SWITCHER_ROW (row), event); + + return; + } + + if (button == GDK_BUTTON_MIDDLE) { + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_CLAIMED); + hdy_tab_view_close_page (self->view, page); + + return; + } + + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_DENIED); +} + +static void +long_press_cb (HdyTabSwitcher *self, + gdouble x, + gdouble y) +{ + GtkListBoxRow *row = gtk_list_box_get_row_at_y (self->list, y); + + if (!row) { + gtk_gesture_set_state (self->click_gesture, GTK_EVENT_SEQUENCE_DENIED); + + return; + } + + do_touch_popup (self, HDY_TAB_SWITCHER_ROW (row)); + + gtk_gesture_set_state (self->long_press_gesture, GTK_EVENT_SEQUENCE_CLAIMED); +} + +static void +hdy_tab_switcher_destroy (GtkWidget *widget) +{ + gtk_container_forall (GTK_CONTAINER (widget), (GtkCallback) gtk_widget_destroy, NULL); + + GTK_WIDGET_CLASS (hdy_tab_switcher_parent_class)->destroy (widget); +} + +static void +hdy_tab_switcher_add (GtkContainer *container, + GtkWidget *widget) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (container); + + if (!self->flap) { + GTK_CONTAINER_CLASS (hdy_tab_switcher_parent_class)->add (container, widget); + + return; + } + + hdy_flap_set_content (self->flap, widget); +} + +static void +hdy_tab_switcher_remove (GtkContainer *container, + GtkWidget *widget) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (container); + + if (widget == GTK_WIDGET (self->flap)) { + GTK_CONTAINER_CLASS (hdy_tab_switcher_parent_class)->remove (container, widget); + + return; + } + + hdy_flap_set_content (self->flap, NULL); +} + +static void +hdy_tab_switcher_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (container); + GtkWidget *content; + + if (include_internals) { + GTK_CONTAINER_CLASS (hdy_tab_switcher_parent_class)->forall (container, + include_internals, + callback, + callback_data); + + return; + } + + if (!self->flap) + return; + + content = hdy_flap_get_content (self->flap); + + if (content) + callback (content, callback_data); +} + +static gboolean +hdy_tab_switcher_popup_menu (GtkWidget *widget) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (widget); + GtkListBoxRow *row = gtk_list_box_get_selected_row (self->list); + + if (row) { + do_popup (self, HDY_TAB_SWITCHER_ROW (row), NULL); + + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static void +set_narrow (HdyTabSwitcher *self, + gboolean narrow) +{ + if (self->narrow == narrow) + return; + + self->narrow = narrow; + + if (!narrow && self->flap) + hdy_tab_switcher_close (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NARROW]); +} + +static void +hdy_tab_switcher_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (widget); + + set_narrow (self, alloc->width < 400); + + GTK_WIDGET_CLASS (hdy_tab_switcher_parent_class)->size_allocate (widget, alloc); +} + +static void +hdy_tab_switcher_dispose (GObject *object) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (object); + + hdy_tab_switcher_set_view (self, NULL); + + g_clear_object (&self->click_gesture); + g_clear_object (&self->long_press_gesture); + + G_OBJECT_CLASS (hdy_tab_switcher_parent_class)->dispose (object); +} + +static void +hdy_tab_switcher_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (object); + + switch (prop_id) { + case PROP_VIEW: + g_value_set_object (value, hdy_tab_switcher_get_view (self)); + break; + case PROP_NARROW: + g_value_set_boolean (value, hdy_tab_switcher_get_narrow (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_switcher_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyTabSwitcher *self = HDY_TAB_SWITCHER (object); + + switch (prop_id) { + case PROP_VIEW: + hdy_tab_switcher_set_view (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_tab_switcher_class_init (HdyTabSwitcherClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->dispose = hdy_tab_switcher_dispose; + object_class->get_property = hdy_tab_switcher_get_property; + object_class->set_property = hdy_tab_switcher_set_property; + + widget_class->destroy = hdy_tab_switcher_destroy; + widget_class->popup_menu = hdy_tab_switcher_popup_menu; + widget_class->size_allocate = hdy_tab_switcher_size_allocate; + + container_class->add = hdy_tab_switcher_add; + container_class->remove = hdy_tab_switcher_remove; + container_class->forall = hdy_tab_switcher_forall; + + /** + * HdyTabSwitcher:view: + * + * The #HdyTabView the tab switcher controls; + * + * Since: 1.4 + */ + props[PROP_VIEW] = + g_param_spec_object ("view", + _("View"), + _("The view the tab switcher controls."), + HDY_TYPE_TAB_VIEW, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_NARROW] = + g_param_spec_boolean ("narrow", + _("Narrow"), + _("Narrow"), + TRUE, + G_PARAM_READABLE); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[SIGNAL_NEW_TAB] = + g_signal_new ("new-tab", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-tab-switcher.ui"); + + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcher, flap); + gtk_widget_class_bind_template_child (widget_class, HdyTabSwitcher, list); + + gtk_widget_class_bind_template_callback (widget_class, reveal_flap_cb); + gtk_widget_class_bind_template_callback (widget_class, collapse_cb); + gtk_widget_class_bind_template_callback (widget_class, new_tab_cb); + gtk_widget_class_bind_template_callback (widget_class, row_selected_cb); + gtk_widget_class_bind_template_callback (widget_class, row_activated_cb); +} + +static void +hdy_tab_switcher_init (HdyTabSwitcher *self) +{ + self->narrow = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->click_gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self->list)); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->click_gesture), 0); + g_signal_connect_swapped (self->click_gesture, "pressed", G_CALLBACK (click_pressed_cb), self); + + self->long_press_gesture = gtk_gesture_long_press_new (GTK_WIDGET (self->list)); + gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->long_press_gesture), TRUE); + g_signal_connect_swapped (self->long_press_gesture, "pressed", G_CALLBACK (long_press_cb), self); +} + +/** + * hdy_tab_switcher_new: + * + * Creates a new #HdyTabSwitcher widget. + * + * Returns: a new #HdyTabSwitcher + * + * Since: 1.4 + */ +GtkWidget * +hdy_tab_switcher_new (void) +{ + return g_object_new (HDY_TYPE_TAB_SWITCHER, NULL); +} + +/** + * hdy_tab_switcher_get_view: + * @self: a #HdyTabSwitcher + * + * Gets the #HdyTabView @self controls. + * + * Returns: (transfer none) (nullable): the #HdyTabView @self controls + * + * Since: 1.4 + */ +HdyTabView * +hdy_tab_switcher_get_view (HdyTabSwitcher *self) +{ + g_return_val_if_fail (HDY_IS_TAB_SWITCHER (self), NULL); + + return self->view; +} + +/** + * hdy_tab_switcher_set_view: + * @self: a #HdyTabSwitcher + * @view: (nullable): a #HdyTabView + * + * Sets the #HdyTabView @self controls. + * + * Since: 1.4 + */ +void +hdy_tab_switcher_set_view (HdyTabSwitcher *self, + HdyTabView *view) +{ + g_return_if_fail (HDY_IS_TAB_SWITCHER (self)); + g_return_if_fail (HDY_IS_TAB_VIEW (view) || view == NULL); + + if (self->view == view) + return; + + if (self->view) { + GListModel *pages = hdy_tab_view_get_pages (self->view); + + g_signal_handlers_disconnect_by_func (self->view, G_CALLBACK (notify_selected_page_cb), self); + g_signal_handlers_disconnect_by_func (pages, G_CALLBACK (pages_changed_cb), self); + } + + g_set_object (&self->view, view); + + if (self->view) { + GListModel *pages = hdy_tab_view_get_pages (self->view); + + g_signal_connect_object (pages, "items-changed", + G_CALLBACK (pages_changed_cb), self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->view, "notify::selected-page", + G_CALLBACK (notify_selected_page_cb), self, + G_CONNECT_SWAPPED); + } + + notify_selected_page_cb (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]); +} + +void +hdy_tab_switcher_open (HdyTabSwitcher *self) +{ + g_return_if_fail (HDY_IS_TAB_SWITCHER (self)); + + hdy_flap_set_reveal_flap (self->flap, TRUE); +} + +void +hdy_tab_switcher_close (HdyTabSwitcher *self) +{ + g_return_if_fail (HDY_IS_TAB_SWITCHER (self)); + + hdy_flap_set_reveal_flap (self->flap, FALSE); +} + +gboolean +hdy_tab_switcher_get_narrow (HdyTabSwitcher *self) +{ + g_return_val_if_fail (HDY_IS_TAB_SWITCHER (self), FALSE); + + return self->narrow; +} diff --git a/src/hdy-tab-switcher.h b/src/hdy-tab-switcher.h new file mode 100644 index 0000000000000000000000000000000000000000..2ee8ccbc5d0bb9a296773bad48705561ebc09e8b --- /dev/null +++ b/src/hdy-tab-switcher.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author: Alexander Mikhaylenko + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only can be included directly." +#endif + +#include "hdy-version.h" + +#include +#include "hdy-tab-view.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_TAB_SWITCHER (hdy_tab_switcher_get_type()) + +HDY_AVAILABLE_IN_1_4 +G_DECLARE_FINAL_TYPE (HdyTabSwitcher, hdy_tab_switcher, HDY, TAB_SWITCHER, GtkBin) + +HDY_AVAILABLE_IN_1_4 +GtkWidget *hdy_tab_switcher_new (void); + +HDY_AVAILABLE_IN_1_4 +HdyTabView *hdy_tab_switcher_get_view (HdyTabSwitcher *self); +HDY_AVAILABLE_IN_1_4 +void hdy_tab_switcher_set_view (HdyTabSwitcher *self, + HdyTabView *view); + +HDY_AVAILABLE_IN_1_4 +void hdy_tab_switcher_open (HdyTabSwitcher *self); +HDY_AVAILABLE_IN_1_4 +void hdy_tab_switcher_close (HdyTabSwitcher *self); + +HDY_AVAILABLE_IN_1_4 +gboolean hdy_tab_switcher_get_narrow (HdyTabSwitcher *self); + +G_END_DECLS diff --git a/src/hdy-tab-switcher.ui b/src/hdy-tab-switcher.ui new file mode 100644 index 0000000000000000000000000000000000000000..1b19d9c23a22d5bf8fcde4f60fa18f088830ec29 --- /dev/null +++ b/src/hdy-tab-switcher.ui @@ -0,0 +1,83 @@ + + + + + diff --git a/src/hdy-version.h.in b/src/hdy-version.h.in index c6f1ade2a77b60b3cbba6ff2c9caac03206cf5e6..b31f3043b4dd0417bccb3118daca1195da478b53 100644 --- a/src/hdy-version.h.in +++ b/src/hdy-version.h.in @@ -90,6 +90,16 @@ */ #define HDY_VERSION_1_2 (HDY_ENCODE_VERSION (1, 2, 0)) +/** + * HDY_VERSION_1_4: + * + * A macro that evaluates to the 1.4 version of Handy, in a format + * that can be used by the C pre-processor. + * + * Since: 1.4 + */ +#define HDY_VERSION_1_4 (HDY_ENCODE_VERSION (1, 4, 0)) + #ifndef _HDY_EXTERN #define _HDY_EXTERN extern #endif @@ -107,11 +117,11 @@ #endif #ifndef HDY_VERSION_MAX_ALLOWED -# define HDY_VERSION_MAX_ALLOWED HDY_VERSION_1_2 +# define HDY_VERSION_MAX_ALLOWED HDY_VERSION_1_4 #endif #ifndef HDY_VERSION_MIN_REQUIRED -# define HDY_VERSION_MIN_REQUIRED HDY_VERSION_1_2 +# define HDY_VERSION_MIN_REQUIRED HDY_VERSION_1_4 #endif #define HDY_UNAVAILABLE(major, minor) G_UNAVAILABLE(major, minor) _HDY_EXTERN @@ -135,3 +145,21 @@ # define HDY_DEPRECATED_TYPE_IN_1_2 # define HDY_DEPRECATED_TYPE_IN_1_2_FOR(f) #endif + +#if HDY_VERSION_MAX_ALLOWED < HDY_VERSION_1_4 +# define HDY_AVAILABLE_IN_1_4 HDY_UNAVAILABLE(1, 4) +#else +# define HDY_AVAILABLE_IN_1_4 _HDY_EXTERN +#endif + +#if HDY_VERSION_MIN_REQUIRED >= HDY_VERSION_1_4 +# define HDY_DEPRECATED_IN_1_4 _HDY_DEPRECATED +# define HDY_DEPRECATED_IN_1_4_FOR(f) _HDY_DEPRECATED_FOR(f) +# define HDY_DEPRECATED_TYPE_IN_1_4 _HDY_DEPRECATED_TYPE +# define HDY_DEPRECATED_TYPE_IN_1_4_FOR(f) _HDY_DEPRECATED_TYPE_FOR(f) +#else +# define HDY_DEPRECATED_IN_1_4 _HDY_EXTERN +# define HDY_DEPRECATED_IN_1_4_FOR(f) _HDY_EXTERN +# define HDY_DEPRECATED_TYPE_IN_1_4 +# define HDY_DEPRECATED_TYPE_IN_1_4_FOR(f) +#endif diff --git a/src/icons/scalable/actions/hdy-list-add-large-symbolic.svg b/src/icons/scalable/actions/hdy-list-add-large-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..b443ba63875c85260b0742e9d8d0d84030217f74 --- /dev/null +++ b/src/icons/scalable/actions/hdy-list-add-large-symbolic.svg @@ -0,0 +1,80 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/icons/scalable/actions/hdy-sheet-collapse-symbolic.svg b/src/icons/scalable/actions/hdy-sheet-collapse-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..5dd07b66fbbb63b493afe66e7536e7eb36a6b384 --- /dev/null +++ b/src/icons/scalable/actions/hdy-sheet-collapse-symbolic.svg @@ -0,0 +1,61 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/icons/scalable/status/hdy-tab-counter-symbolic.svg b/src/icons/scalable/status/hdy-tab-counter-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..5837085c4161aaa9bb1ab1c7e58d762f5e9561a1 --- /dev/null +++ b/src/icons/scalable/status/hdy-tab-counter-symbolic.svg @@ -0,0 +1,69 @@ + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + diff --git a/src/icons/scalable/status/hdy-tab-overflow-symbolic.svg b/src/icons/scalable/status/hdy-tab-overflow-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..602b7ce2cb435716e4d20ea37faca3d12a6d0144 --- /dev/null +++ b/src/icons/scalable/status/hdy-tab-overflow-symbolic.svg @@ -0,0 +1,41 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + diff --git a/src/meson.build b/src/meson.build index c201f34e40f6ef86746fefd49cc08207cfa638f4..56067e2b783afa7d6aa82ce6018406e784da748d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -96,6 +96,8 @@ src_headers = [ 'hdy-swipe-tracker.h', 'hdy-swipeable.h', 'hdy-tab-bar.h', + 'hdy-tab-button.h', + 'hdy-tab-switcher.h', 'hdy-tab-view.h', 'hdy-title-bar.h', 'hdy-types.h', @@ -155,12 +157,16 @@ src_sources = [ 'hdy-squeezer.c', 'hdy-stackable-box.c', 'hdy-status-page.c', + 'hdy-swipe-away-bin.c', 'hdy-swipe-group.c', 'hdy-swipe-tracker.c', 'hdy-swipeable.c', 'hdy-tab.c', 'hdy-tab-bar.c', 'hdy-tab-box.c', + 'hdy-tab-button.c', + 'hdy-tab-switcher.c', + 'hdy-tab-switcher-row.c', 'hdy-tab-view.c', 'hdy-title-bar.c', 'hdy-value-object.c', diff --git a/src/themes/Adwaita-dark.css b/src/themes/Adwaita-dark.css index 973ff2a3083970b92a5d4f4612f576493bfe632d..abeb0a60bcedcd9ba083d84a39774065177c4c67 100644 --- a/src/themes/Adwaita-dark.css +++ b/src/themes/Adwaita-dark.css @@ -105,6 +105,10 @@ preferencesgroup > box > box:not(:first-child) { margin-top: 12px; } tabbar .tab-indicator:not(.clickable) { background: none; box-shadow: none; border-color: transparent; } +.tab-button label { font-weight: 800; font-size: 8pt; } + +.tab-button label.small { font-size: 6pt; } + /*************************** Check and Radio buttons * */ popover.combo list { min-width: 200px; } @@ -294,8 +298,44 @@ tabbar tab, .tab-drag-icon tab { padding: 6px; } tabbar tab.needs-attention, .tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), rgba(21, 83, 158, 0.2) 15%, rgba(21, 83, 158, 0) 15%); } -tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { padding: 0; margin: 0; min-width: 24px; min-height: 24px; border-radius: 99px; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; background: none; } +tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 24px; min-height: 24px; } + +tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator:hover { background: alpha(currentColor, 0.15); } + +tabbar tab .tab-close-button:hover:active, tabbar tab .tab-indicator:hover:active, .tab-drag-icon tab .tab-close-button:hover:active, .tab-drag-icon tab .tab-indicator:hover:active { background: alpha(black, 0.2); } + +tabbar tab .tab-indicator:not(.clickable), .tab-drag-icon tab .tab-indicator:not(.clickable) { background: none; } + +.tab-switcher { min-height: 180px; } + +.tab-switcher list { padding-top: 48px; padding-bottom: 84px; } + +.tab-switcher list row { background: none; border: none; box-shadow: none; padding: 0; outline-color: transparent; color: white; } + +.tab-switcher list row .content { min-height: 40px; margin-left: 5px; margin-right: 5px; margin-bottom: 2px; padding: 3px; border-radius: 5px; -gtk-outline-radius: 5px; border: none; box-shadow: none; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +.tab-switcher list row .content:dir(ltr) { padding-left: 11px; } + +.tab-switcher list row .content:dir(rtl) { padding-right: 11px; } + +.tab-switcher list row:selected .content { background: alpha(currentColor, 0.06); } + +.tab-switcher list row:hover .content { background: alpha(currentColor, 0.1); } + +.tab-switcher list row:active .content { background: alpha(black, 0.15); } + +.tab-switcher list row.needs-attention { color: #99c1f1; } + +.tab-switcher list row .close-btn, .tab-switcher list row .indicator-btn { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 36px; min-height: 36px; } + +.tab-switcher list row .close-btn:hover, .tab-switcher list row .indicator-btn:hover { background: alpha(currentColor, 0.15); } + +.tab-switcher list row .close-btn:hover:active, .tab-switcher list row .indicator-btn:hover:active { background: alpha(black, 0.2); } + +.tab-switcher .collapse-button { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; padding: 6px 30px; margin: 6px; } + +.tab-switcher .collapse-button:hover { background: alpha(currentColor, 0.15); } -tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator.clickable:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator.clickable:hover { background: alpha(#eeeeec, 0.15); } +.tab-switcher .collapse-button:hover:active { background: alpha(black, 0.2); } -tabbar tab .tab-close-button:active, tabbar tab .tab-indicator.clickable:active, .tab-drag-icon tab .tab-close-button:active, .tab-drag-icon tab .tab-indicator.clickable:active { background: alpha(black, 0.2); } +.tab-switcher .new-tab-button { min-width: 48px; min-height: 48px; border-radius: 100%; -gtk-outline-radius: 100%; padding: 0; margin: 18px; } diff --git a/src/themes/Adwaita.css b/src/themes/Adwaita.css index 3c43bde6b1e753e74afa76df3b37479fc2c6560e..f2d4aa1d904f9e114ae91dc03b33c030810739b0 100644 --- a/src/themes/Adwaita.css +++ b/src/themes/Adwaita.css @@ -105,6 +105,10 @@ preferencesgroup > box > box:not(:first-child) { margin-top: 12px; } tabbar .tab-indicator:not(.clickable) { background: none; box-shadow: none; border-color: transparent; } +.tab-button label { font-weight: 800; font-size: 8pt; } + +.tab-button label.small { font-size: 6pt; } + /*************************** Check and Radio buttons * */ popover.combo list { min-width: 200px; } @@ -294,8 +298,44 @@ tabbar tab, .tab-drag-icon tab { padding: 6px; } tabbar tab.needs-attention, .tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), rgba(53, 132, 228, 0.2) 15%, rgba(53, 132, 228, 0) 15%); } -tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { padding: 0; margin: 0; min-width: 24px; min-height: 24px; border-radius: 99px; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; background: none; } +tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 24px; min-height: 24px; } + +tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator:hover { background: alpha(currentColor, 0.15); } + +tabbar tab .tab-close-button:hover:active, tabbar tab .tab-indicator:hover:active, .tab-drag-icon tab .tab-close-button:hover:active, .tab-drag-icon tab .tab-indicator:hover:active { background: alpha(black, 0.2); } + +tabbar tab .tab-indicator:not(.clickable), .tab-drag-icon tab .tab-indicator:not(.clickable) { background: none; } + +.tab-switcher { min-height: 180px; } + +.tab-switcher list { padding-top: 48px; padding-bottom: 84px; } + +.tab-switcher list row { background: none; border: none; box-shadow: none; padding: 0; outline-color: transparent; color: black; } + +.tab-switcher list row .content { min-height: 40px; margin-left: 5px; margin-right: 5px; margin-bottom: 2px; padding: 3px; border-radius: 5px; -gtk-outline-radius: 5px; border: none; box-shadow: none; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +.tab-switcher list row .content:dir(ltr) { padding-left: 11px; } + +.tab-switcher list row .content:dir(rtl) { padding-right: 11px; } + +.tab-switcher list row:selected .content { background: alpha(currentColor, 0.06); } + +.tab-switcher list row:hover .content { background: alpha(currentColor, 0.1); } + +.tab-switcher list row:active .content { background: alpha(black, 0.15); } + +.tab-switcher list row.needs-attention { color: #1c71d8; } + +.tab-switcher list row .close-btn, .tab-switcher list row .indicator-btn { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 36px; min-height: 36px; } + +.tab-switcher list row .close-btn:hover, .tab-switcher list row .indicator-btn:hover { background: alpha(currentColor, 0.15); } + +.tab-switcher list row .close-btn:hover:active, .tab-switcher list row .indicator-btn:hover:active { background: alpha(black, 0.2); } + +.tab-switcher .collapse-button { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; padding: 6px 30px; margin: 6px; } + +.tab-switcher .collapse-button:hover { background: alpha(currentColor, 0.15); } -tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator.clickable:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator.clickable:hover { background: alpha(#2e3436, 0.15); } +.tab-switcher .collapse-button:hover:active { background: alpha(black, 0.2); } -tabbar tab .tab-close-button:active, tabbar tab .tab-indicator.clickable:active, .tab-drag-icon tab .tab-close-button:active, .tab-drag-icon tab .tab-indicator.clickable:active { background: alpha(black, 0.2); } +.tab-switcher .new-tab-button { min-width: 48px; min-height: 48px; border-radius: 100%; -gtk-outline-radius: 100%; padding: 0; margin: 18px; } diff --git a/src/themes/HighContrast.css b/src/themes/HighContrast.css index c5b1f1a3d2a209e2174072ee87756ea5a581eaf6..5007e36c90e1211dac4be22bba1fe585c2bd9721 100644 --- a/src/themes/HighContrast.css +++ b/src/themes/HighContrast.css @@ -105,6 +105,10 @@ preferencesgroup > box > box:not(:first-child) { margin-top: 12px; } tabbar .tab-indicator:not(.clickable) { background: none; box-shadow: none; border-color: transparent; } +.tab-button label { font-weight: 800; font-size: 8pt; } + +.tab-button label.small { font-size: 6pt; } + /*************************** Check and Radio buttons * */ popover.combo list { min-width: 200px; } @@ -294,8 +298,44 @@ tabbar tab, .tab-drag-icon tab { padding: 6px; } tabbar tab.needs-attention, .tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), rgba(27, 106, 203, 0.2) 15%, rgba(27, 106, 203, 0) 15%); } -tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { padding: 0; margin: 0; min-width: 24px; min-height: 24px; border-radius: 99px; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; background: none; } +tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 24px; min-height: 24px; } + +tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator:hover { background: alpha(currentColor, 0.15); } + +tabbar tab .tab-close-button:hover:active, tabbar tab .tab-indicator:hover:active, .tab-drag-icon tab .tab-close-button:hover:active, .tab-drag-icon tab .tab-indicator:hover:active { background: alpha(black, 0.2); } + +tabbar tab .tab-indicator:not(.clickable), .tab-drag-icon tab .tab-indicator:not(.clickable) { background: none; } + +.tab-switcher { min-height: 180px; } + +.tab-switcher list { padding-top: 48px; padding-bottom: 84px; } + +.tab-switcher list row { background: none; border: none; box-shadow: none; padding: 0; outline-color: transparent; color: black; } + +.tab-switcher list row .content { min-height: 40px; margin-left: 5px; margin-right: 5px; margin-bottom: 2px; padding: 3px; border-radius: 5px; -gtk-outline-radius: 5px; border: none; box-shadow: none; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +.tab-switcher list row .content:dir(ltr) { padding-left: 11px; } + +.tab-switcher list row .content:dir(rtl) { padding-right: 11px; } + +.tab-switcher list row:selected .content { background: alpha(currentColor, 0.06); } + +.tab-switcher list row:hover .content { background: alpha(currentColor, 0.1); } + +.tab-switcher list row:active .content { background: alpha(black, 0.15); } + +.tab-switcher list row.needs-attention { color: #1c71d8; } + +.tab-switcher list row .close-btn, .tab-switcher list row .indicator-btn { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 36px; min-height: 36px; } + +.tab-switcher list row .close-btn:hover, .tab-switcher list row .indicator-btn:hover { background: alpha(currentColor, 0.15); } + +.tab-switcher list row .close-btn:hover:active, .tab-switcher list row .indicator-btn:hover:active { background: alpha(black, 0.2); } + +.tab-switcher .collapse-button { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; padding: 6px 30px; margin: 6px; } + +.tab-switcher .collapse-button:hover { background: alpha(currentColor, 0.15); } -tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator.clickable:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator.clickable:hover { background: alpha(#272c2e, 0.15); } +.tab-switcher .collapse-button:hover:active { background: alpha(black, 0.2); } -tabbar tab .tab-close-button:active, tabbar tab .tab-indicator.clickable:active, .tab-drag-icon tab .tab-close-button:active, .tab-drag-icon tab .tab-indicator.clickable:active { background: alpha(black, 0.2); } +.tab-switcher .new-tab-button { min-width: 48px; min-height: 48px; border-radius: 100%; -gtk-outline-radius: 100%; padding: 0; margin: 18px; } diff --git a/src/themes/HighContrastInverse.css b/src/themes/HighContrastInverse.css index e43782c88c3cd3502e008aea9ff5328fe6c00582..41bbb90d0e314e85f5e41a9f9df232f7ff2ec11a 100644 --- a/src/themes/HighContrastInverse.css +++ b/src/themes/HighContrastInverse.css @@ -105,6 +105,10 @@ preferencesgroup > box > box:not(:first-child) { margin-top: 12px; } tabbar .tab-indicator:not(.clickable) { background: none; box-shadow: none; border-color: transparent; } +.tab-button label { font-weight: 800; font-size: 8pt; } + +.tab-button label.small { font-size: 6pt; } + /*************************** Check and Radio buttons * */ popover.combo list { min-width: 200px; } @@ -294,8 +298,44 @@ tabbar tab, .tab-drag-icon tab { padding: 6px; } tabbar tab.needs-attention, .tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), rgba(15, 59, 113, 0.2) 15%, rgba(15, 59, 113, 0) 15%); } -tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { padding: 0; margin: 0; min-width: 24px; min-height: 24px; border-radius: 99px; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; background: none; } +tabbar tab .tab-close-button, tabbar tab .tab-indicator, .tab-drag-icon tab .tab-close-button, .tab-drag-icon tab .tab-indicator { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 24px; min-height: 24px; } + +tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator:hover { background: alpha(currentColor, 0.15); } + +tabbar tab .tab-close-button:hover:active, tabbar tab .tab-indicator:hover:active, .tab-drag-icon tab .tab-close-button:hover:active, .tab-drag-icon tab .tab-indicator:hover:active { background: alpha(black, 0.2); } + +tabbar tab .tab-indicator:not(.clickable), .tab-drag-icon tab .tab-indicator:not(.clickable) { background: none; } + +.tab-switcher { min-height: 180px; } + +.tab-switcher list { padding-top: 48px; padding-bottom: 84px; } + +.tab-switcher list row { background: none; border: none; box-shadow: none; padding: 0; outline-color: transparent; color: white; } + +.tab-switcher list row .content { min-height: 40px; margin-left: 5px; margin-right: 5px; margin-bottom: 2px; padding: 3px; border-radius: 5px; -gtk-outline-radius: 5px; border: none; box-shadow: none; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +.tab-switcher list row .content:dir(ltr) { padding-left: 11px; } + +.tab-switcher list row .content:dir(rtl) { padding-right: 11px; } + +.tab-switcher list row:selected .content { background: alpha(currentColor, 0.06); } + +.tab-switcher list row:hover .content { background: alpha(currentColor, 0.1); } + +.tab-switcher list row:active .content { background: alpha(black, 0.15); } + +.tab-switcher list row.needs-attention { color: #99c1f1; } + +.tab-switcher list row .close-btn, .tab-switcher list row .indicator-btn { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; min-width: 36px; min-height: 36px; } + +.tab-switcher list row .close-btn:hover, .tab-switcher list row .indicator-btn:hover { background: alpha(currentColor, 0.15); } + +.tab-switcher list row .close-btn:hover:active, .tab-switcher list row .indicator-btn:hover:active { background: alpha(black, 0.2); } + +.tab-switcher .collapse-button { background: none; border: none; box-shadow: none; -gtk-icon-shadow: none; text-shadow: none; border-radius: 99px; -gtk-outline-radius: 99px; color: inherit; padding: 0; padding: 6px 30px; margin: 6px; } + +.tab-switcher .collapse-button:hover { background: alpha(currentColor, 0.15); } -tabbar tab .tab-close-button:hover, tabbar tab .tab-indicator.clickable:hover, .tab-drag-icon tab .tab-close-button:hover, .tab-drag-icon tab .tab-indicator.clickable:hover { background: alpha(#f3f3f1, 0.15); } +.tab-switcher .collapse-button:hover:active { background: alpha(black, 0.2); } -tabbar tab .tab-close-button:active, tabbar tab .tab-indicator.clickable:active, .tab-drag-icon tab .tab-close-button:active, .tab-drag-icon tab .tab-indicator.clickable:active { background: alpha(black, 0.2); } +.tab-switcher .new-tab-button { min-width: 48px; min-height: 48px; border-radius: 100%; -gtk-outline-radius: 100%; padding: 0; margin: 18px; } diff --git a/src/themes/_Adwaita-base.scss b/src/themes/_Adwaita-base.scss index bd32b63164280be1a566fdc89caac4f57a0f2ea5..fac6c8834109252e3e916e8bbfc6488bdf5ee610 100644 --- a/src/themes/_Adwaita-base.scss +++ b/src/themes/_Adwaita-base.scss @@ -5,6 +5,26 @@ $tab_bg: darken($bg_color, if($variant == 'dark', 6%, 12%)); $tab_bg_backdrop: darken($backdrop_bg_color, 6%); +@mixin flat_round_button() { + background: none; + border: none; + box-shadow: none; + -gtk-icon-shadow: none; + text-shadow: none; + border-radius: 99px; + -gtk-outline-radius: 99px; + color: inherit; + padding: 0; + + &:hover { + background: hdyalpha(currentColor, .15); + + &:active { + background: hdyalpha(black, .2); + } + } +} + // HdyComboRow popover.combo { @@ -536,28 +556,86 @@ tabbar, .tab-close-button, .tab-indicator { - padding: 0; - margin: 0; + @include flat_round_button(); min-width: 24px; min-height: 24px; - border-radius: 99px; + } - border: none; - box-shadow: none; - -gtk-icon-shadow: none; - text-shadow: none; + .tab-indicator:not(.clickable) { background: none; } + } +} - .tab-close-button, - .tab-indicator.clickable { - &:hover { - background: hdyalpha($fg_color, .15); +.tab-switcher { + min-height: 180px; + + list { + padding-top: 48px; + padding-bottom: 84px; + + row { + background: none; + border: none; + box-shadow: none; + padding: 0; + outline-color: transparent; + color: $text_color; + + .content { + min-height: 40px; + margin-left: 5px; + margin-right: 5px; + margin-bottom: 2px; + padding: 3px; + border-radius: 5px; + -gtk-outline-radius: 5px; + border: none; + box-shadow: none; + transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + + &:dir(ltr) { padding-left: 11px; } + &:dir(rtl) { padding-right: 11px; } + } + + &:selected .content { + background: hdyalpha(currentColor, .06); + } + + &:hover .content { + background: hdyalpha(currentColor, .1); } - &:active { - background: hdyalpha(black, .2); + &:active .content { + background: hdyalpha(black, .15); + } + + &.needs-attention { + // Blue 4 | Blue 1 + color: if($variant == 'light', #1c71d8, #99c1f1); + } + + .close-btn, + .indicator-btn { + @include flat_round_button(); + min-width: 36px; + min-height: 36px; } } } + + .collapse-button { + @include flat_round_button(); + padding: 6px 30px; + margin: 6px; + } + + .new-tab-button { + min-width: 48px; + min-height: 48px; + border-radius: 100%; + -gtk-outline-radius: 100%; + padding: 0; + margin: 18px; + } } diff --git a/src/themes/_fallback-base.scss b/src/themes/_fallback-base.scss index dc9a47b27ac62f46d2506da5a587dc87637b255f..e934f36ca4110dad4b663a245eff21eb2ee0e648 100644 --- a/src/themes/_fallback-base.scss +++ b/src/themes/_fallback-base.scss @@ -209,3 +209,12 @@ tabbar .tab-indicator:not(.clickable) { box-shadow: none; border-color: transparent; } + +.tab-button label { + font-weight: 800; + font-size: 8pt; + + &.small { + font-size: 6pt; + } +} diff --git a/src/themes/fallback.css b/src/themes/fallback.css index d2587b17e3153cf653ece1556880357727f2223d..733b5df32858b89cafdb3753dcaf79e6da017847 100644 --- a/src/themes/fallback.css +++ b/src/themes/fallback.css @@ -104,3 +104,7 @@ preferencesgroup > box > label:not(:first-child) { margin-top: 6px; } preferencesgroup > box > box:not(:first-child) { margin-top: 12px; } tabbar .tab-indicator:not(.clickable) { background: none; box-shadow: none; border-color: transparent; } + +.tab-button label { font-weight: 800; font-size: 8pt; } + +.tab-button label.small { font-size: 6pt; }