Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc6d5d3
gwl: Add initial overflow support through sliding
anaximeno Jan 10, 2026
c6d8ccb
gwl: Scroll to focused app when a new one is added or when changing w…
anaximeno Jan 11, 2026
f7d01cb
gwl: Avoid trying to remove slide timer multiple times
anaximeno Jan 11, 2026
a79bb46
gwl: Properly integrate with the scroll-behavior settings
anaximeno Jan 11, 2026
6f440aa
gwl: Avoid trying to remove the scroll to focused app deboucer timer …
anaximeno Jan 11, 2026
9839b65
gwl: Listen to the scrollBox allocation signal to update the scroll b…
anaximeno Jan 11, 2026
c0798a7
gwl: Make sure to scroll to the focused app group on workspace switched
anaximeno Jan 11, 2026
9a5efcc
gwl: Refactor the scroll logic to its own class
anaximeno Jan 17, 2026
a86bdc8
gwl: Improve scroll to item implementation
anaximeno Jan 17, 2026
6b737e3
gwl: Extract app group scroll box to its own module
anaximeno Jan 17, 2026
c3039af
gwl: Disable actions if panel edit mode is enabled and update classe…
anaximeno Jan 17, 2026
82630e9
gwl: Debounce calls to updateScrollVisibility to avoid it being calle…
anaximeno Jan 18, 2026
051b7ec
gwl: Define style in proper cinnamon theme file instead of in-code
anaximeno Jan 18, 2026
8895d86
gwl: Update scroll button theme
anaximeno Jan 19, 2026
b0bdc3c
gwl: Implementation using the native St.ScrollView
anaximeno Jan 21, 2026
ece5b29
gwl: Update scroll police depending on the orientation and make sure …
anaximeno Jan 21, 2026
d2caa0d
gwl: Fix handleDragOver positioning for the scrollview based implemen…
anaximeno Jan 21, 2026
dc16a31
Update scrollbox vfade/hfade theme defaults
anaximeno Jan 31, 2026
f70951b
gwl: Adjust min_size setting for lateral panels
anaximeno Feb 9, 2026
8198512
gwl: Only set appGroup focus if app actually has focus
anaximeno Feb 16, 2026
162eb58
gwl: Improve the scroll to last focused app
anaximeno Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions data/theme/cinnamon-sass/widgets/_windowlist.scss
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@
&-notifications-badge-label {
font-size: 12px;
}

&-scrollbox {
&.vfade {
-st-vfade-offset: 68px !important;
-st-hfade-offset: 0 !important;
}
&.hfade {
-st-hfade-offset: 68px !important;
-st-vfade-offset: 0 !important;
}
}
}

// classic window list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,6 @@ class AppGroup {
getPreferredWidth(actor, forHeight, alloc) {
const [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_width(forHeight);
const [labelMinSize, labelNaturalSize] = this.label.get_preferred_width(forHeight);
// The label text starts in the center of the icon, so we should allocate the space
// needed for the icon plus the space needed for(label - icon/2)
alloc.min_size = 1 * global.ui_scale;

const {appId} = this.groupState;

const allocateForLabel = this.labelVisiblePref ||
Expand All @@ -392,16 +388,18 @@ class AppGroup {
} else {
alloc.natural_size = iconNaturalSize + 6 * global.ui_scale;
}
alloc.min_size = alloc.natural_size;
} else {
alloc.natural_size = this.state.trigger('getPanelHeight');
alloc.min_size = 1 * global.ui_scale;
}
}

getPreferredHeight(actor, forWidth, alloc) {
let [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_height(forWidth);
let [labelMinSize, labelNaturalSize] = this.label.get_preferred_height(forWidth);
alloc.min_size = Math.min(iconMinSize, labelMinSize);
alloc.natural_size = Math.max(iconNaturalSize, labelNaturalSize);
alloc.min_size = alloc.natural_size;
}

allocate(actor, box, flags) {
Expand Down Expand Up @@ -554,7 +552,7 @@ class AppGroup {
}

onEnter() {
if (this.state.panelEditMode) return false;
if (this.state.panelEditMode || this.state.scrollActive) return false;

this.actor.add_style_pseudo_class('hover');

Expand Down Expand Up @@ -641,7 +639,7 @@ class AppGroup {
const {appId, metaWindows, lastFocused} = this.groupState;

if (hasFocus === undefined) {
hasFocus = this.workspaceState.lastFocusedApp === appId;
hasFocus = this.workspaceState.lastFocusedApp === appId && getFocusState(lastFocused);
}

// If any of the windows associated with our app have focus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class PinnedFavs {
const currentWorkspace = this.params.state.trigger('getCurrentWorkspace');
const newFavorites = [];
let refActorFound = false;
currentWorkspace.actor.get_children().forEach( (actor, i) => {
currentWorkspace.container.get_children().forEach( (actor, i) => {
const appGroup = currentWorkspace.appGroups.find( appGroup => appGroup.actor === actor );
if (!appGroup) return;
const {app, appId, isFavoriteApp} = appGroup.groupState;
Expand Down Expand Up @@ -793,14 +793,26 @@ class GroupedWindowListApplet extends Applet.Applet {
&& St.Widget.get_default_direction () === St.TextDirection.RTL;

const axis = this.state.isHorizontal ? [x, 'x2'] : [y, 'y2'];

let adjustmentValue = 0;
if (currentWorkspace.scrollBox) {
const adjustment = this.state.isHorizontal ?
currentWorkspace.scrollBox.actor.get_hscroll_bar().get_adjustment() :
currentWorkspace.scrollBox.actor.get_vscroll_bar().get_adjustment();
adjustmentValue = adjustment.get_value();
}

// Add scroll position to current coordinate
axis[0] += adjustmentValue;

if(rtl_horizontal)
axis[0] = this.actor.width - axis[0];

// save data on drag start
if(this.state.dragging.posList === null){
this.state.dragging.isForeign = !(source instanceof AppGroup);
this.state.dragging.posList = [];
currentWorkspace.actor.get_children().forEach( child => {
currentWorkspace.container.get_children().forEach( child => {
let childPos;
if(rtl_horizontal)
childPos = this.actor.width - child.get_allocation_box()['x1'];
Expand All @@ -814,7 +826,7 @@ class GroupedWindowListApplet extends Applet.Applet {
let pos = 0;
while(pos < this.state.dragging.posList.length && axis[0] > this.state.dragging.posList[pos])
pos++;

let favLength = 0;
for (const appGroup of currentWorkspace.appGroups) {
if(appGroup.groupState.isFavoriteApp)
Expand All @@ -834,21 +846,21 @@ class GroupedWindowListApplet extends Applet.Applet {

if(this.state.dragging.isForeign) {
if (this.state.dragging.dragPlaceholder)
currentWorkspace.actor.set_child_at_index(this.state.dragging.dragPlaceholder.actor, pos);
currentWorkspace.container.set_child_at_index(this.state.dragging.dragPlaceholder.actor, pos);
else {
const iconSize = this.getPanelIconSize() * global.ui_scale;
this.state.dragging.dragPlaceholder = new DND.GenericDragPlaceholderItem();
this.state.dragging.dragPlaceholder.child.width = iconSize;
this.state.dragging.dragPlaceholder.child.height = iconSize;
currentWorkspace.actor.insert_child_at_index(
currentWorkspace.container.insert_child_at_index(
this.state.dragging.dragPlaceholder.actor,
this.state.dragging.pos
);
this.state.dragging.dragPlaceholder.animateIn();
}
}
else
currentWorkspace.actor.set_child_at_index(source.actor, pos);
currentWorkspace.container.set_child_at_index(source.actor, pos);
}

if(this.state.dragging.isForeign)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib;
const St = imports.gi.St;
const { SignalManager } = imports.misc.signalManager;

const EDGE_SCROLL_ZONE_SIZE = 68;
const EDGE_SCROLL_SPEED = 8;
const EDGE_SCROLL_INTERVAL = 16;

class ScrollBox {
constructor(state) {
this.state = state;
this.signals = new SignalManager(null);

this.actor = new St.ScrollView({
x_expand: true,
y_expand: true,
reactive: true
});

this.actor.set_auto_scrolling(false);
this.actor.set_mouse_scrolling(true);
this.actor.set_clip_to_allocation(true);
this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL);

this.box = new St.BoxLayout({
vertical: !this.state.isHorizontal,
style_class: 'grouped-window-list-scrollbox-container'
});

this.actor.add_actor(this.box);

this.edgeScrollTimeoutId = 0;
this.edgeScrollDirection = 0;
this.scrollActiveTimeoutId = 0;

this.signals.connect(this.actor, 'scroll-event', (actor, event) => this._onScroll(actor, event));
this.signals.connect(this.actor, 'motion-event', (actor, event) => this._onMotionEvent(actor, event));
this.signals.connect(this.actor, 'leave-event', () => this._stopEdgeScroll());

this.stateConnectionID = this.state.connect({
orientation: (state) => this.on_orientation_changed()
});

this.on_orientation_changed();
}

destroy() {
this._stopEdgeScroll();
if (this.stateConnectionID) {
this.state.disconnect(this.stateConnectionID);
}
this.signals.disconnectAllSignals();
this.actor.destroy();
}

on_orientation_changed() {
this.box.vertical = !this.state.isHorizontal;
if (this.state.isHorizontal) {
this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL);
this.actor.style_class = 'grouped-window-list-scrollbox hfade';
} else {
this.actor.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL);
this.actor.style_class = 'grouped-window-list-scrollbox vfade';
}
}

scrollToChild(childActor) {
if (!childActor) return;

// Get allocation of child relative to container
const allocation = childActor.get_allocation_box();

// Child coordinates
let c1, c2;

const isHorizontal = this.state.isHorizontal;
let adjustment;

if (isHorizontal) {
c1 = allocation.x1;
c2 = allocation.x2;
const hBar = this.actor.get_hscroll_bar();
if (hBar) adjustment = hBar.get_adjustment();
} else {
c1 = allocation.y1;
c2 = allocation.y2;
const vBar = this.actor.get_vscroll_bar();
if (vBar) adjustment = vBar.get_adjustment();
}

if (adjustment) {
const current = adjustment.value;
const page_size = adjustment.page_size;

let fade_offset = 30;

const fade_eff = this.actor.get_effect('fade');

if (fade_eff) {
fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset;
}

if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) {
const newValue = (c1 + c2) / 2 - page_size / 2;
adjustment.value = Math.max(adjustment.lower, Math.min(newValue, adjustment.upper - page_size));
}
}
}

_onMotionEvent(actor, event) {
if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE;

const [x, y] = event.get_coords();
const [actorX, actorY] = actor.get_transformed_position();
const [actorWidth, actorHeight] = actor.get_transformed_size();

// Calculate relative position within the actor
const relX = x - actorX;
const relY = y - actorY;

let scrollDirection = 0;
const adjustment = this._getScrollAdjustment();

if (!adjustment) return Clutter.EVENT_PROPAGATE;

// Check if we can scroll (content is larger than view)
const canScroll = adjustment.upper > adjustment.page_size;
if (!canScroll) {
this._stopEdgeScroll();
return Clutter.EVENT_PROPAGATE;
}

if (this.state.isHorizontal) {
// Check left edge
if (relX < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) {
scrollDirection = -1;
}
// Check right edge
else if (relX > actorWidth - EDGE_SCROLL_ZONE_SIZE &&
adjustment.value < adjustment.upper - adjustment.page_size) {
scrollDirection = 1;
}
} else {
// Check top edge
if (relY < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) {
scrollDirection = -1;
}
// Check bottom edge
else if (relY > actorHeight - EDGE_SCROLL_ZONE_SIZE &&
adjustment.value < adjustment.upper - adjustment.page_size) {
scrollDirection = 1;
}
}

if (scrollDirection !== 0 && scrollDirection !== this.edgeScrollDirection) {
this._startEdgeScroll(scrollDirection);
} else if (scrollDirection === 0) {
this._stopEdgeScroll();
}

return Clutter.EVENT_PROPAGATE;
}

_getScrollAdjustment() {
if (this.state.isHorizontal) {
const hBar = this.actor.get_hscroll_bar();
return hBar ? hBar.get_adjustment() : null;
} else {
const vBar = this.actor.get_vscroll_bar();
return vBar ? vBar.get_adjustment() : null;
}
}

_startEdgeScroll(direction) {
this._stopEdgeScroll();
this.edgeScrollDirection = direction;
this.state.scrollActive = true;

this.edgeScrollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, EDGE_SCROLL_INTERVAL, () => {
const adjustment = this._getScrollAdjustment();
if (!adjustment) {
this.edgeScrollTimeoutId = 0;
return GLib.SOURCE_REMOVE;
}

const newValue = adjustment.value + (EDGE_SCROLL_SPEED * this.edgeScrollDirection * global.ui_scale);

// Clamp value to valid range
adjustment.value = Math.max(adjustment.lower,
Math.min(newValue, adjustment.upper - adjustment.page_size));

return GLib.SOURCE_CONTINUE;
});
}

_stopEdgeScroll() {
if (this.edgeScrollTimeoutId > 0) {
GLib.source_remove(this.edgeScrollTimeoutId);
this.edgeScrollTimeoutId = 0;
}
this.edgeScrollDirection = 0;

if (this.scrollActiveTimeoutId) {
GLib.source_remove(this.scrollActiveTimeoutId);
}

this.scrollActiveTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => {
if (this.edgeScrollDirection === 0) {
this.state.scrollActive = false;
}
this.scrollActiveTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
}

_onScroll(actor, event) {
if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE;

// Handle horizontal scrolling with vertical wheel
if (this.state.isHorizontal) {
const direction = event.get_scroll_direction();
let delta = 0;

const hBar = this.actor.get_hscroll_bar();
if (!hBar) return Clutter.EVENT_PROPAGATE;

const adjustment = hBar.get_adjustment();
if (!adjustment) return Clutter.EVENT_PROPAGATE;

if (direction === Clutter.ScrollDirection.UP) {
delta = -10 * global.ui_scale;
} else if (direction === Clutter.ScrollDirection.DOWN) {
delta = 10 * global.ui_scale;
} else if (direction === Clutter.ScrollDirection.SMOOTH) {
const [dx, dy] = event.get_scroll_delta();
// If pure vertical scroll, map to horizontal
if (Math.abs(dy) > Math.abs(dx)) {
delta = dy * (16 * global.ui_scale); // Scale factor
} else {
// Let StScrollView handle horizontal smooth scroll naturally if it exists
return Clutter.EVENT_PROPAGATE;
}
} else {
return Clutter.EVENT_PROPAGATE;
}

if (delta !== 0) {
// Manually updating adjustment value using property
adjustment.value = adjustment.value + delta;
return Clutter.EVENT_STOP;
}
}
return Clutter.EVENT_PROPAGATE;
}
}

module.exports = ScrollBox;
Loading
Loading