feat(ags): use hyprland animations for popup windows
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2024-04-07 14:29:12 -04:00
parent 4e7904775b
commit 22722c27c4
19 changed files with 103 additions and 314 deletions

View file

@ -23,7 +23,6 @@ animations {
enabled = yes
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
bezier = easeInOutBack, 0.68, -0.6, 0.32, 1.6
bezier = easeInBack, 0.36, 0, 0.66, -0.56
bezier = easeOutBack, 0.34, 1.56, 0.64, 1

View file

@ -120,12 +120,12 @@ Var<Widget>,
'is_listening' | 'is_polling' | 'value',
Widget[]
>;
export type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' |
'slide right' | 'popin' | 'fade';
export type CloseType = 'none' | 'stay' | 'released' | 'clicked';
export type PopupWindowProps<Child extends Widget, Attr = unknown> =
WindowProps<Child> & {
transition?: RevealerProps<Widget>['transition']
transition_duration?: number
bezier?: string
transition?: HyprTransition;
on_open?(self: PopupWindow<Child, Attr>): void
on_close?(self: PopupWindow<Child, Attr>): void
blur?: boolean

View file

@ -54,7 +54,7 @@ export default (app: Application) => {
attribute: { app },
on_primary_click_release: () => {
App.closeWindow('applauncher');
App.closeWindow('win-applauncher');
app.launch();
},

View file

@ -71,7 +71,7 @@ const Applauncher = (window_name = 'applauncher') => {
const appList = Applications.query(text || '');
if (appList[0]) {
App.closeWindow(window_name);
App.closeWindow(`win-${window_name}`);
appList[0].launch();
}
},
@ -112,7 +112,7 @@ const Applauncher = (window_name = 'applauncher') => {
setup: (self) => {
self.hook(App, (_, name, visible) => {
if (name !== window_name) {
if (name !== `win-${window_name}`) {
return;
}
@ -150,6 +150,7 @@ const Applauncher = (window_name = 'applauncher') => {
export default () => PopupWindow({
name: 'applauncher',
transition: 'slide top',
keymode: 'on-demand',
content: Applauncher(),
});

View file

@ -16,7 +16,6 @@ export default () => BarRevealer({
monitor: 1,
exclusivity: 'exclusive',
anchor: ['bottom', 'left', 'right'],
transition: 'slide_up',
bar: Box({
vertical: true,
children: [

View file

@ -38,7 +38,7 @@ Hyprland.connect('event', (hyprObj) => {
}
});
export default ({ bar, transition, monitor = 0, ...rest }) => {
export default ({ anchor, bar, monitor = 0, ...rest }) => {
const BarVisible = Variable(true);
FullscreenState.connect('changed', (v) => {
@ -83,9 +83,25 @@ export default ({ bar, transition, monitor = 0, ...rest }) => {
visible: BarVisible.bind().as((v) => !v),
});
const rev = Revealer({
transition,
const vertical = anchor.includes('left') && anchor.includes('right');
const isBottomOrLeft = (
anchor.includes('left') && anchor.includes('right') && anchor.includes('bottom')
) || (
anchor.includes('top') && anchor.includes('bottom') && anchor.includes('left')
);
let transition: 'slide_up' | 'slide_down' | 'slide_left' | 'slide_right';
if (vertical) {
transition = isBottomOrLeft ? 'slide_up' : 'slide_down';
}
else {
transition = isBottomOrLeft ? 'slide_right' : 'slide_left';
}
const barWrap = Revealer({
reveal_child: BarVisible.bind(),
transition,
child: bar,
});
@ -94,6 +110,7 @@ export default ({ bar, transition, monitor = 0, ...rest }) => {
layer: 'overlay',
monitor,
margins: [-1, -1, -1, -1],
anchor,
...rest,
attribute: {
@ -105,13 +122,11 @@ export default ({ bar, transition, monitor = 0, ...rest }) => {
css: 'min-height: 1px; padding: 1px;',
hexpand: true,
hpack: 'fill',
vertical: transition === 'slide_up' ||
transition === 'slide_down',
vertical,
children: transition === 'slide_up' ||
transition === 'slide_left' ?
[buffer, rev] :
[rev, buffer],
children: isBottomOrLeft ?
[buffer, barWrap] :
[barWrap, buffer],
}),
}).on('enter-notify-event', () => {
if (!BarVisible.value) {

View file

@ -5,11 +5,11 @@ import Clock from './clock';
export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: () => App.toggleWindow('calendar'),
on_primary_click_release: () => App.toggleWindow('win-calendar'),
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'calendar') {
if (windowName === 'win-calendar') {
self.toggleClassName('toggle-on', visible);
}
});

View file

@ -14,18 +14,18 @@ export default () => CursorBox({
class_name: 'toggle-off',
on_primary_click_release: (self) => {
(App.getWindow('notification-center') as PopupWindow)
(App.getWindow('win-notification-center') as PopupWindow)
.set_x_pos(
self.get_allocation(),
'right',
);
App.toggleWindow('notification-center');
App.toggleWindow('win-notification-center');
},
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'notification-center') {
if (windowName === 'win-notification-center') {
self.toggleClassName('toggle-on', visible);
}
});

View file

@ -32,18 +32,18 @@ export default () => {
class_name: 'toggle-off',
on_primary_click_release: (self) => {
(App.getWindow('quick-settings') as PopupWindow)
(App.getWindow('win-quick-settings') as PopupWindow)
.set_x_pos(
self.get_allocation(),
'right',
);
App.toggleWindow('quick-settings');
App.toggleWindow('win-quick-settings');
},
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName === 'quick-settings') {
if (windowName === 'win-quick-settings') {
self.toggleClassName('toggle-on', visible);
}
});

View file

@ -21,7 +21,6 @@ const SPACING = 12;
export default () => BarRevealer({
anchor: ['top', 'left', 'right'],
exclusivity: 'exclusive',
transition: 'slide_down',
bar: CenterBox({
css: 'margin: 5px 5px 5px 5px',
class_name: 'bar',

View file

@ -1,8 +1,7 @@
import Gtk from 'gi://Gtk?version=3.0';
const Hyprland = await Service.import('hyprland');
const { Box, Overlay, register } = Widget;
const { Box, register } = Widget;
const { timeout } = Utils;
// Types
@ -11,15 +10,10 @@ import { Variable as Var } from 'types/variable';
import {
CloseType,
BoxGeneric,
OverlayGeneric,
PopupChild,
HyprTransition,
PopupWindowProps,
} from 'global-types';
// FIXME: deal with overlay children?
// TODO: make props changes affect the widget
export class PopupWindow<
Child extends Gtk.Widget,
@ -34,9 +28,8 @@ export class PopupWindow<
}
#content: Var<Gtk.Widget>;
#antiClip: Var<boolean>;
#needsAnticlipping: boolean;
#close_on_unfocus: CloseType;
#transition: HyprTransition;
get content() {
return this.#content.value;
@ -51,14 +44,22 @@ export class PopupWindow<
return this.#close_on_unfocus;
}
set close_on_unfocus(value: 'none' | 'stay' | 'released' | 'clicked') {
set close_on_unfocus(value: CloseType) {
this.#close_on_unfocus = value;
}
get transition() {
return this.#transition;
}
set transition(t: HyprTransition) {
this.#transition = t;
Hyprland.messageAsync(`keyword layerrule animation ${t}, ${this.name}`);
}
constructor({
transition = 'slide_down',
transition = 'slide top',
transition_duration = 800,
bezier = 'cubic-bezier(0.68, -0.4, 0.32, 1.4)',
on_open = () => {/**/},
on_close = () => {/**/},
@ -73,10 +74,7 @@ export class PopupWindow<
close_on_unfocus = 'released',
...rest
}: PopupWindowProps<Child, Attr>) {
const needsAnticlipping = bezier.match(/-[0-9]/) !== null &&
transition !== 'crossfade';
const contentVar = Variable(Box() as Gtk.Widget);
const antiClip = Variable(false);
if (content) {
contentVar.setValue(content);
@ -84,19 +82,16 @@ export class PopupWindow<
super({
...rest,
name,
name: `win-${name}`,
visible,
anchor,
layer,
attribute,
setup: () => {
const id = App.connect('config-parsed', () => {
// Set close delay dynamically
App.closeWindowDelay[name] = transition_duration;
// Add way to make window open on startup
if (visible) {
App.openWindow(`${name}`);
App.openWindow(`win-${name}`);
}
// This connection should always run only once
@ -105,266 +100,33 @@ export class PopupWindow<
if (blur) {
Hyprland.messageAsync('[[BATCH]] ' +
`keyword layerrule ignorealpha[0.97],${name}; ` +
`keyword layerrule blur,${name}`);
`keyword layerrule ignorealpha 0.97, win-${name}; ` +
`keyword layerrule blur, win-${name}`);
}
Hyprland.messageAsync(
`keyword layerrule animation ${transition}, win-${name}`,
);
},
child: Overlay({
overlays: [Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
setup: (self) => {
// Make sure child doesn't
// get bigger than it should
const MAX_ANCHORS = 4;
child: contentVar.bind(),
});
self.hpack = 'center';
self.vpack = 'center';
if (anchor.includes('top') &&
anchor.includes('bottom')) {
self.vpack = 'center';
}
else if (anchor.includes('top')) {
self.vpack = 'start';
}
else if (anchor.includes('bottom')) {
self.vpack = 'end';
}
if (anchor.includes('left') &&
anchor.includes('right')) {
self.hpack = 'center';
}
else if (anchor.includes('left')) {
self.hpack = 'start';
}
else if (anchor.includes('right')) {
self.hpack = 'end';
}
if (anchor.length === MAX_ANCHORS) {
self.hpack = 'center';
self.vpack = 'center';
}
if (needsAnticlipping) {
const reorder_child = (position: number) => {
// If unanchored, we have another anticlip widget
// so we can't change the order
if (anchor.length !== 0) {
for (const ch of self.children) {
if (ch !== contentVar.value) {
self.reorder_child(ch, position);
return;
}
}
}
};
self.hook(antiClip, () => {
if (transition === 'slide_down') {
self.vertical = true;
reorder_child(-1);
}
else if (transition === 'slide_up') {
self.vertical = true;
reorder_child(0);
}
else if (transition === 'slide_right') {
self.vertical = false;
reorder_child(-1);
}
else if (transition === 'slide_left') {
self.vertical = false;
reorder_child(0);
}
});
}
},
children: contentVar.bind().transform((v) => {
if (needsAnticlipping) {
return [
// Add an anticlip widget when unanchored
// to not have a weird animation
anchor.length === 0 && Box({
css: `
min-height: 100px;
min-width: 100px;
padding: 2px;
`,
visible: antiClip.bind(),
}),
v,
Box({
css: `
min-height: 100px;
min-width: 100px;
padding: 2px;
`,
visible: antiClip.bind(),
}),
];
}
else {
return [v];
}
}) as PopupChild,
})],
setup: (self) => {
self.on('get-child-position', (_, ch) => {
const overlay = contentVar.value
.get_parent() as OverlayGeneric;
if (ch === overlay) {
const alloc = overlay.get_allocation();
(self.child as BoxGeneric).css = `
min-height: ${alloc.height}px;
min-width: ${alloc.width}px;
`;
}
this.hook(App, (_, currentName, isOpen) => {
if (currentName === `win-${name}`) {
if (isOpen) {
on_open(this);
}
else {
timeout(Number(transition_duration), () => {
on_close(this);
});
},
child: Box({
css: `
min-height: 1px;
min-width: 1px;
padding: 1px;
`,
setup: (self) => {
let currentTimeout: number;
self.hook(App, (_, currentName, isOpen) => {
if (currentName === name) {
const overlay = contentVar.value
.get_parent() as OverlayGeneric;
const alloc = overlay.get_allocation();
const height = antiClip ?
alloc.height + 100 + 10 :
alloc.height + 10;
if (needsAnticlipping) {
antiClip.setValue(true);
const thisTimeout = timeout(
transition_duration,
() => {
// Only run the timeout if there isn't a newer timeout
if (thisTimeout ===
currentTimeout) {
antiClip.setValue(false);
}
},
);
currentTimeout = thisTimeout;
}
let css = '';
/* Margin: top | right | bottom | left */
switch (transition) {
case 'slide_down':
css = `margin:
-${height}px
0
${height}px
0
;`;
break;
case 'slide_up':
css = `margin:
${height}px
0
-${height}px
0
;`;
break;
case 'slide_left':
css = `margin:
0
-${height}px
0
${height}px
;`;
break;
case 'slide_right':
css = `margin:
0
${height}px
0
-${height}px
;`;
break;
case 'crossfade':
css = `
opacity: 0;
min-height: 1px;
min-width: 1px;
`;
break;
default:
break;
}
if (isOpen) {
on_open(this);
// To get the animation, we need to set the css
// to hide the widget and then timeout to have
// the animation
overlay.css = css;
timeout(10, () => {
overlay.css = `
transition: margin
${transition_duration}ms
${bezier},
opacity
${transition_duration}ms
${bezier};
`;
});
}
else {
timeout(transition_duration, () => {
on_close(this);
});
overlay.css = `${css}
transition: margin
${transition_duration}ms ${bezier},
opacity
${transition_duration}ms ${bezier};
`;
}
}
});
},
}),
}),
}
}
});
this.#content = contentVar;
this.#close_on_unfocus = close_on_unfocus;
this.#needsAnticlipping = needsAnticlipping;
this.#antiClip = antiClip;
this.#transition = transition;
}
set_x_pos(

View file

@ -9,6 +9,7 @@ import PopupWindow from '../misc/popup.ts';
export const NotifPopups = () => Window({
name: 'notifications',
anchor: ['bottom', 'left'],
layer: 'overlay',
monitor: 1,
child: PopUpsWidget(),
@ -18,7 +19,7 @@ export const NotifPopups = () => Window({
export const NotifCenter = () => PopupWindow({
name: 'notification-center',
anchor: ['bottom', 'right'],
transition: 'slide_up',
transition: 'slide bottom',
monitor: 1,
content: NotifCenterWidget(),

View file

@ -76,7 +76,7 @@ const ClearButton = () => CursorBox({
on_primary_click_release: () => {
Notifications.clear();
timeout(1000, () => App.closeWindow('notification-center'));
timeout(1000, () => App.closeWindow('win-notification-center'));
},
setup: (self) => {

View file

@ -8,6 +8,7 @@ import PopupWindow from '../misc/popup.ts';
export const NotifPopups = () => Window({
name: 'notifications',
layer: 'overlay',
anchor: ['top', 'left'],
child: PopUpsWidget(),
});

View file

@ -41,6 +41,8 @@ const OSDs = () => {
}),
);
stack.show_all();
// Delay popup method so it
// doesn't show any OSDs at launch
timeout(1000, () => {
@ -49,13 +51,13 @@ const OSDs = () => {
stack.attribute.popup = (osd: string) => {
++count;
stack.shown = osd;
App.openWindow('osd');
App.openWindow('win-osd');
timeout(HIDE_DELAY, () => {
--count;
if (count === 0) {
App.closeWindow('osd');
App.closeWindow('win-osd');
}
});
};
@ -71,8 +73,6 @@ export default () => PopupWindow({
anchor: ['bottom'],
exclusivity: 'ignore',
close_on_unfocus: 'stay',
transition: 'slide_up',
transition_duration,
bezier: 'ease',
transition: 'slide bottom',
content: OSDs(),
});

View file

@ -44,5 +44,6 @@ const PowermenuWidget = () => CenterBox({
export default () => PopupWindow({
name: 'powermenu',
transition: 'slide bottom',
content: PowermenuWidget(),
});

View file

@ -327,7 +327,7 @@ const SecondRow = () => Row({
command: () => {
execAsync(['lock']).catch(print);
},
secondary_command: () => App.openWindow('powermenu'),
secondary_command: () => App.openWindow('win-powermenu'),
icon: 'system-lock-screen-symbolic',
}),
],

View file

@ -26,7 +26,7 @@ export default () => {
name: 'openAppLauncher',
gesture: 'UD',
edge: 'T',
command: () => App.openWindow('applauncher'),
command: () => App.openWindow('win-applauncher'),
});
TouchGestures.addGesture({

View file

@ -94,19 +94,30 @@ in {
wayland.windowManager.hyprland = {
settings = {
animations.animation = [
# Ags takes care of doing the animations
"layers, 0"
animations = {
bezier = [
"easeInOutBack, 0.68, -0.6, 0.32, 1.6"
];
animation = [
"fadeLayersIn, 0"
"fadeLayersOut, 1, 3000, default"
"layers, 1, 8, easeInOutBack, slide left"
];
};
layerrule = [
"noanim, ^(?!win-).*"
];
exec-once = [
"ags"
"sleep 3; ags -r 'App.openWindow(\"applauncher\")'"
"sleep 3; ags -r 'App.openWindow(\"win-applauncher\")'"
];
bind = [
"$mainMod SHIFT, E, exec, ags -t powermenu"
"$mainMod , D, exec, ags -t applauncher"
"$mainMod SHIFT, E, exec, ags -t win-powermenu"
"$mainMod , D, exec, ags -t win-applauncher"
];
binde = [
## Brightness control