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: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2902>
This commit is contained in:
Georges Basile Stavracas Neto 2023-08-10 10:27:13 -03:00 committed by Marge Bot
parent d4f331d14a
commit ca503774b2
2 changed files with 179 additions and 11 deletions

View File

@ -44,7 +44,18 @@ $panel_transition_duration: 250ms; // same as the overview transition duration
} }
&#panelActivities { &#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 // screen activity indicators
@ -90,6 +101,10 @@ $panel_transition_duration: 250ms; // same as the overview transition duration
.panel-button { .panel-button {
@include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color); @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color);
&#panelActivities .workspace-dot {
background-color: $panel_system_fg_color;
}
// clock // clock
&.clock-display { &.clock-display {
@include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color, $highlighted_child: true, $child_class:".clock"); @include panel_button($panel_system_fg_color, $fg:$panel_system_fg_color, $highlighted_child: true, $child_class:".clock");

View File

@ -4,6 +4,7 @@ import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter'; import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib'; import GLib from 'gi://GLib';
import GObject from 'gi://GObject'; import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta'; import Meta from 'gi://Meta';
import Shell from 'gi://Shell'; import Shell from 'gi://Shell';
import St from 'gi://St'; import St from 'gi://St';
@ -18,6 +19,7 @@ import * as PopupMenu from './popupMenu.js';
import * as PanelMenu from './panelMenu.js'; import * as PanelMenu from './panelMenu.js';
import {QuickSettingsMenu, SystemIndicator} from './quickSettings.js'; import {QuickSettingsMenu, SystemIndicator} from './quickSettings.js';
import * as Main from './main.js'; import * as Main from './main.js';
import * as Util from '../misc/util.js';
import * as RemoteAccessStatus from './status/remoteAccess.js'; import * as RemoteAccessStatus from './status/remoteAccess.js';
import * as PowerProfileStatus from './status/powerProfiles.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 N_QUICK_SETTINGS_COLUMNS = 2;
const INACTIVE_WORKSPACE_DOT_SCALE = 0.75;
/** /**
* AppMenuButton: * 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( const ActivitiesButton = GObject.registerClass(
class ActivitiesButton extends PanelMenu.Button { class ActivitiesButton extends PanelMenu.Button {
_init() { _init() {
super._init(0.0, null, true); super._init(0.0, null, true);
this.accessible_role = Atk.Role.TOGGLE_BUTTON;
this.name = 'panelActivities'; this.set({
name: 'panelActivities',
/* Translators: If there is no suitable word for "Activities" accessible_role: Atk.Role.TOGGLE_BUTTON,
in your language, you can use the word for "Overview". */ /* Translators: If there is no suitable word for "Activities"
this._label = new St.Label({ in your language, you can use the word for "Overview". */
text: _('Activities'), accessible_name: _('Activities'),
y_align: Clutter.ActorAlign.CENTER,
}); });
this.add_actor(this._label);
this.label_actor = this._label; this.add_child(new WorkspaceIndicators());
Main.overview.connect('showing', () => { Main.overview.connect('showing', () => {
this.add_style_pseudo_class('checked'); this.add_style_pseudo_class('checked');