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 enabled = yes
bezier = myBezier, 0.05, 0.9, 0.1, 1.05 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 = easeInBack, 0.36, 0, 0.66, -0.56
bezier = easeOutBack, 0.34, 1.56, 0.64, 1 bezier = easeOutBack, 0.34, 1.56, 0.64, 1

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,7 @@
import Gtk from 'gi://Gtk?version=3.0'; import Gtk from 'gi://Gtk?version=3.0';
const Hyprland = await Service.import('hyprland'); const Hyprland = await Service.import('hyprland');
const { Box, Overlay, register } = Widget; const { Box, register } = Widget;
const { timeout } = Utils; const { timeout } = Utils;
// Types // Types
@ -11,15 +10,10 @@ import { Variable as Var } from 'types/variable';
import { import {
CloseType, CloseType,
BoxGeneric, HyprTransition,
OverlayGeneric,
PopupChild,
PopupWindowProps, PopupWindowProps,
} from 'global-types'; } from 'global-types';
// FIXME: deal with overlay children?
// TODO: make props changes affect the widget
export class PopupWindow< export class PopupWindow<
Child extends Gtk.Widget, Child extends Gtk.Widget,
@ -34,9 +28,8 @@ export class PopupWindow<
} }
#content: Var<Gtk.Widget>; #content: Var<Gtk.Widget>;
#antiClip: Var<boolean>;
#needsAnticlipping: boolean;
#close_on_unfocus: CloseType; #close_on_unfocus: CloseType;
#transition: HyprTransition;
get content() { get content() {
return this.#content.value; return this.#content.value;
@ -51,14 +44,22 @@ export class PopupWindow<
return this.#close_on_unfocus; 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; 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({ constructor({
transition = 'slide_down', transition = 'slide top',
transition_duration = 800, transition_duration = 800,
bezier = 'cubic-bezier(0.68, -0.4, 0.32, 1.4)',
on_open = () => {/**/}, on_open = () => {/**/},
on_close = () => {/**/}, on_close = () => {/**/},
@ -73,10 +74,7 @@ export class PopupWindow<
close_on_unfocus = 'released', close_on_unfocus = 'released',
...rest ...rest
}: PopupWindowProps<Child, Attr>) { }: PopupWindowProps<Child, Attr>) {
const needsAnticlipping = bezier.match(/-[0-9]/) !== null &&
transition !== 'crossfade';
const contentVar = Variable(Box() as Gtk.Widget); const contentVar = Variable(Box() as Gtk.Widget);
const antiClip = Variable(false);
if (content) { if (content) {
contentVar.setValue(content); contentVar.setValue(content);
@ -84,19 +82,16 @@ export class PopupWindow<
super({ super({
...rest, ...rest,
name, name: `win-${name}`,
visible, visible,
anchor, anchor,
layer, layer,
attribute, attribute,
setup: () => { setup: () => {
const id = App.connect('config-parsed', () => { const id = App.connect('config-parsed', () => {
// Set close delay dynamically
App.closeWindowDelay[name] = transition_duration;
// Add way to make window open on startup // Add way to make window open on startup
if (visible) { if (visible) {
App.openWindow(`${name}`); App.openWindow(`win-${name}`);
} }
// This connection should always run only once // This connection should always run only once
@ -105,266 +100,33 @@ export class PopupWindow<
if (blur) { if (blur) {
Hyprland.messageAsync('[[BATCH]] ' + Hyprland.messageAsync('[[BATCH]] ' +
`keyword layerrule ignorealpha[0.97],${name}; ` + `keyword layerrule ignorealpha 0.97, win-${name}; ` +
`keyword layerrule blur,${name}`); `keyword layerrule blur, win-${name}`);
} }
Hyprland.messageAsync(
`keyword layerrule animation ${transition}, win-${name}`,
);
}, },
child: Overlay({ child: contentVar.bind(),
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;
self.hpack = 'center'; this.hook(App, (_, currentName, isOpen) => {
self.vpack = 'center'; if (currentName === `win-${name}`) {
if (isOpen) {
if (anchor.includes('top') && on_open(this);
anchor.includes('bottom')) { }
self.vpack = 'center'; else {
} timeout(Number(transition_duration), () => {
else if (anchor.includes('top')) { on_close(this);
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;
`;
}
}); });
}, }
}
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.#content = contentVar;
this.#close_on_unfocus = close_on_unfocus; this.#close_on_unfocus = close_on_unfocus;
this.#needsAnticlipping = needsAnticlipping; this.#transition = transition;
this.#antiClip = antiClip;
} }
set_x_pos( set_x_pos(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,19 +94,30 @@ in {
wayland.windowManager.hyprland = { wayland.windowManager.hyprland = {
settings = { settings = {
animations.animation = [ animations = {
# Ags takes care of doing the animations bezier = [
"layers, 0" "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 = [ exec-once = [
"ags" "ags"
"sleep 3; ags -r 'App.openWindow(\"applauncher\")'" "sleep 3; ags -r 'App.openWindow(\"win-applauncher\")'"
]; ];
bind = [ bind = [
"$mainMod SHIFT, E, exec, ags -t powermenu" "$mainMod SHIFT, E, exec, ags -t win-powermenu"
"$mainMod , D, exec, ags -t applauncher" "$mainMod , D, exec, ags -t win-applauncher"
]; ];
binde = [ binde = [
## Brightness control ## Brightness control