From ca503774b22c15b4a86cfeb0feb0a47829a621e6 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 10 Aug 2023 10:27:13 -0300 Subject: [PATCH] panel: Add workspaces indicators in activities button After removing the app name and icon, the next natural step that was requested from the design team is to add workspaces indicators to the top bar, where currently the Activities button is placed. In addition to that, this is desired because there are known issues with using "Activities" as a label for the overview. A more comprehensive rationale for that can be found at [1]. Add an workspaces indicator replacing the Activities label in the activities button. The WorkspaceIndicators class controls how many workspaces dots exists, their expansion, and the width multiplier. The WorkspaceDot class takes the expansion and the multiplier, and applies it internally so that we can get perfectly rounded dots at all times without using CSS hacks. The width multipliers are hardcoded, and defined by the design team. We can revisit them later if necessary. Special care is taken to not let these width multipliers result in fractional widths. When the number of workspaces changes, WorkspaceIndicators adds new dot to the end, and animate them. When removing, scale the dot out, then destroy it. This does not work with workspace grids, but that's not supported by GNOME Shell anyway, so no effort is made to cover this use case. The button continues to have "Activities" as its accessible name, but the label actor is removed. Also adjust the padding of the activities pill, so it better wraps the new indicators. [1] https://gitlab.gnome.org/Teams/Design/os-mockups/-/issues/227 Part-of: --- .../gnome-shell-sass/widgets/_panel.scss | 17 +- js/ui/panel.js | 173 +++++++++++++++++- 2 files changed, 179 insertions(+), 11 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_panel.scss b/data/theme/gnome-shell-sass/widgets/_panel.scss index d36e3dc3c..f23aa4b81 100644 --- a/data/theme/gnome-shell-sass/widgets/_panel.scss +++ b/data/theme/gnome-shell-sass/widgets/_panel.scss @@ -44,7 +44,18 @@ $panel_transition_duration: 250ms; // same as the overview transition duration } &#panelActivities { - -natural-hpadding: $base_padding * 3; + -natural-hpadding: $base_padding * 2.5; + + & StBoxLayout { + spacing: 5px; + } + + & .workspace-dot { + border-radius: 999px; + min-width: 8px; + min-height: 8px; + background-color: $panel_fg_color; + } } // screen activity indicators @@ -90,6 +101,10 @@ $panel_transition_duration: 250ms; // same as the overview transition duration .panel-button { @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color); + &#panelActivities .workspace-dot { + background-color: $panel_system_fg_color; + } + // clock &.clock-display { @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color, $highlighted_child: true, $child_class:".clock"); diff --git a/js/ui/panel.js b/js/ui/panel.js index 590078608..fe16fab46 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -4,6 +4,7 @@ import Atk from 'gi://Atk'; import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; +import Graphene from 'gi://Graphene'; import Meta from 'gi://Meta'; import Shell from 'gi://Shell'; import St from 'gi://St'; @@ -18,6 +19,7 @@ import * as PopupMenu from './popupMenu.js'; import * as PanelMenu from './panelMenu.js'; import {QuickSettingsMenu, SystemIndicator} from './quickSettings.js'; import * as Main from './main.js'; +import * as Util from '../misc/util.js'; import * as RemoteAccessStatus from './status/remoteAccess.js'; import * as PowerProfileStatus from './status/powerProfiles.js'; @@ -47,6 +49,8 @@ const BUTTON_DND_ACTIVATION_TIMEOUT = 250; const N_QUICK_SETTINGS_COLUMNS = 2; +const INACTIVE_WORKSPACE_DOT_SCALE = 0.75; + /** * AppMenuButton: * @@ -256,23 +260,172 @@ const AppMenuButton = GObject.registerClass({ } }); +const WorkspaceDot = GObject.registerClass({ + Properties: { + 'expansion': GObject.ParamSpec.double('expansion', '', '', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, 0.0), + 'width-multiplier': GObject.ParamSpec.double( + 'width-multiplier', '', '', + GObject.ParamFlags.READWRITE, + 1.0, 10.0, 1.0), + }, +}, class WorkspaceDot extends Clutter.Actor { + constructor(params = {}) { + super({ + pivot_point: new Graphene.Point({x: 0.5, y: 0.5}), + ...params, + }); + + this._dot = new St.Widget({ + style_class: 'workspace-dot', + y_align: Clutter.ActorAlign.CENTER, + pivot_point: new Graphene.Point({x: 0.5, y: 0.5}), + request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT, + }); + this.add_child(this._dot); + + this.connect('notify::width-multiplier', () => this.queue_relayout()); + this.connect('notify::expansion', () => { + this._updateVisuals(); + this.queue_relayout(); + }); + this._updateVisuals(); + + this._destroying = false; + } + + _updateVisuals() { + const {expansion} = this; + + this._dot.set({ + opacity: Util.lerp(0.50, 1.0, expansion) * 255, + scaleX: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion), + scaleY: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion), + }); + } + + vfunc_get_preferred_width(forHeight) { + const factor = Util.lerp(1.0, this.widthMultiplier, this.expansion); + return this._dot.get_preferred_width(forHeight).map(v => Math.round(v * factor)); + } + + vfunc_get_preferred_height(forWidth) { + return this._dot.get_preferred_height(forWidth); + } + + vfunc_allocate(box) { + this.set_allocation(box); + + box.set_origin(0, 0); + this._dot.allocate(box); + } + + scaleIn() { + this.set({ + scale_x: 0, + scale_y: 0, + }); + + this.ease({ + duration: 500, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + scale_x: 1.0, + scale_y: 1.0, + }); + } + + scaleOutAndDestroy() { + this._destroying = true; + + this.ease({ + duration: 500, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC, + scale_x: 0.0, + scale_y: 0.0, + onComplete: () => this.destroy(), + }); + } + + get destroying() { + return this._destroying; + } +}); + +const WorkspaceIndicators = GObject.registerClass( +class WorkspaceIndicators extends St.BoxLayout { + constructor() { + super(); + + this._workspacesAdjustment = Main.createWorkspacesAdjustment(this); + this._workspacesAdjustment.connectObject( + 'notify::value', () => this._updateExpansion(), + 'notify::upper', () => this._recalculateDots(), + this); + + for (let i = 0; i < this._workspacesAdjustment.upper; i++) + this.insert_child_at_index(new WorkspaceDot(), i); + this._updateExpansion(); + } + + _getActiveIndicators() { + return [...this].filter(i => !i.destroying); + } + + _recalculateDots() { + const activeIndicators = this._getActiveIndicators(); + const nIndicators = activeIndicators.length; + const targetIndicators = this._workspacesAdjustment.upper; + + let remaining = Math.abs(nIndicators - targetIndicators); + while (remaining--) { + if (nIndicators < targetIndicators) { + const indicator = new WorkspaceDot(); + this.add_child(indicator); + indicator.scaleIn(); + } else { + const indicator = activeIndicators[nIndicators - remaining - 1]; + indicator.scaleOutAndDestroy(); + } + } + + this._updateExpansion(); + } + + _updateExpansion() { + const nIndicators = this._getActiveIndicators().length; + const activeWorkspace = this._workspacesAdjustment.value; + + let widthMultiplier; + if (nIndicators <= 2) + widthMultiplier = 3.625; + else if (nIndicators <= 5) + widthMultiplier = 3.25; + else + widthMultiplier = 2.75; + + this.get_children().forEach((indicator, index) => { + const distance = Math.abs(index - activeWorkspace); + indicator.expansion = Math.clamp(1 - distance, 0, 1); + indicator.widthMultiplier = widthMultiplier; + }); + } +}); + const ActivitiesButton = GObject.registerClass( class ActivitiesButton extends PanelMenu.Button { _init() { super._init(0.0, null, true); - this.accessible_role = Atk.Role.TOGGLE_BUTTON; - this.name = 'panelActivities'; - - /* Translators: If there is no suitable word for "Activities" - in your language, you can use the word for "Overview". */ - this._label = new St.Label({ - text: _('Activities'), - y_align: Clutter.ActorAlign.CENTER, + this.set({ + name: 'panelActivities', + accessible_role: Atk.Role.TOGGLE_BUTTON, + /* Translators: If there is no suitable word for "Activities" + in your language, you can use the word for "Overview". */ + accessible_name: _('Activities'), }); - this.add_actor(this._label); - this.label_actor = this._label; + this.add_child(new WorkspaceIndicators()); Main.overview.connect('showing', () => { this.add_style_pseudo_class('checked');