feat(ags popup): allow setting custom bezier for windows
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2024-01-02 18:37:12 -05:00
parent 60615feeb9
commit 111a065293
2 changed files with 321 additions and 65 deletions

View file

@ -1,55 +1,74 @@
import App from 'resource:///com/github/Aylur/ags/app.js'; import App from 'resource:///com/github/Aylur/ags/app.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Revealer, Box, Window } from 'resource:///com/github/Aylur/ags/widget.js'; import { Box, Overlay, Window } from 'resource:///com/github/Aylur/ags/widget.js';
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js'; import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
/** /**
* @typedef {import('types/widgets/revealer').RevealerProps} RevProp * @typedef {import('types/widgets/revealer').RevealerProps} RevProps
* @typedef {import('types/widgets/window').WindowProps} WinProp * @typedef {import('types/widgets/window').WindowProps} WinProps
* @typedef {import('types/widgets/window').default} Window
* @typedef {import('types/widgets/box').default} Box
* @typedef {import('gi://Gtk').Gtk.Widget} Widget
*/ */
// FIXME: deal with overlay children?
// TODO: make this a new class to be able to edit props
/** /**
* @param {WinProp & { * @param {WinProps & {
* transition?: RevProp['transition'] * transition?: RevProps['transition']
* transition_duration?: RevProp['transition_duration'] * transition_duration?: number
* bezier?: string
* on_open?: function * on_open?: function
* on_close?: function * on_close?: function
* blur?: boolean * blur?: boolean
* close_on_unfocus?: 'none'|'stay'|'released'|'clicked' * close_on_unfocus?: 'none'|'stay'|'released'|'clicked'
* anchor?: Array<string>
* name: string
* }} o * }} o
*/ */
export default ({ export default ({
transition = 'slide_down', transition = 'slide_down',
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 = () => {/**/},
// Window props // Window props
name, name,
child, child = Box(),
visible = false, visible = false,
anchor = [],
layer = 'overlay', layer = 'overlay',
blur = false, blur = false,
close_on_unfocus = 'released', close_on_unfocus = 'released',
...props ...props
}) => { }) => {
const window = Window({ const Child = Variable(child);
name, const AntiClip = Variable(false);
layer,
visible: false,
...props,
attribute: { const needsAnticlipping = bezier.match(/-[0-9]/) !== null &&
transition !== 'crossfade';
const attribute = {
/** /**
* @param {typeof imports.gi.Gtk.Allocation} alloc * @param {import('gi://Gtk').Gtk.Allocation} alloc
* @param {'left'|'right'} side * @param {'left'|'right'} side
*/ */
set_x_pos: ( set_x_pos: (
alloc, alloc,
side = 'right', side = 'right',
) => { ) => {
/** @type Window */
// @ts-expect-error
const window = App.getWindow(name);
if (!window) {
return;
}
const width = window.get_display() const width = window.get_display()
.get_monitor_at_point(alloc.x, alloc.y) .get_monitor_at_point(alloc.x, alloc.y)
.get_geometry().width; .get_geometry().width;
@ -69,26 +88,44 @@ export default ({
]; ];
}, },
// @ts-expect-error get_child: () => Child.value,
get_child: () => window.child.children[0].child,
/** @param {import('types/widget').Widget} new_child */ /** @param {Widget} new_child */
set_child: (new_child) => { set_child: (new_child) => {
// @ts-expect-error Child.value = new_child;
window.child.children[0].child = new_child; App.getWindow(name)?.child.show_all();
// @ts-expect-error
window.child.children[0].show_all();
}, },
// This is for my custom pointers.js // This is for my custom pointers.js
close_on_unfocus, close_on_unfocus,
}, };
if (transition === 'none') {
return Window({
name,
layer,
anchor,
visible: false,
...props,
attribute,
child: Child.bind(),
});
}
const window = Window({
name,
layer,
anchor,
visible: false,
...props,
attribute,
setup: () => { setup: () => {
// Add way to make window open on startup // Add way to make window open on startup
const id = App.connect('config-parsed', () => { const id = App.connect('config-parsed', () => {
if (visible) { if (visible) {
App.openWindow(String(name)); App.openWindow(`${name}`);
} }
App.disconnect(id); App.disconnect(id);
}); });
@ -100,39 +137,257 @@ export default ({
} }
}, },
// Wrapping the revealer inside a box is needed child: Overlay({
// to allocate some space for it even when not revealed overlays: [Box({
setup: (self) => {
// Make sure child doesn't
// get bigger than it should
const MAX_ANCHORS = 4;
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) {
/** @param {number} position */
const reorder_child = (position) => {
// 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 !== Child.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);
}
});
}
},
// @ts-expect-error
children: Child.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];
}
}),
})],
setup: (self) => {
self.on('get-child-position', (_, ch) => {
/** @type Box */
// @ts-expect-error
const sizeBox = self.child;
// @ts-expect-error
const overlay = Child.value.get_parent();
if (ch === overlay) {
const alloc = overlay.get_allocation();
const setAlloc = /** @param {number} v */
(v) => v - 2 < 0 ? 1 : v;
sizeBox.css = `
min-height: ${setAlloc(alloc.height - 2)}px;
min-width: ${setAlloc(alloc.width - 2)}px;
`;
}
});
},
child: Box({ child: Box({
css: ` css: `
min-height: 1px; min-height: 1px;
min-width: 1px; min-width: 1px;
padding: 1px; padding: 1px;
`, `,
child: Revealer({
transition,
transition_duration,
setup: (self) => { setup: (self) => {
let currentTimeout;
self.hook(App, (_, currentName, isOpen) => { self.hook(App, (_, currentName, isOpen) => {
if (currentName === name) { if (currentName === name) {
self.reveal_child = isOpen; // @ts-expect-error
const overlay = Child.value.get_parent();
const alloc = overlay.get_allocation();
const height = needsAnticlipping ?
alloc.height + 100 + 10 :
alloc.height + 10;
if (needsAnticlipping) {
AntiClip.value = true;
const thisTimeout = timeout(
transition_duration,
() => {
// Only run the timeout if there isn't a newer timeout
if (thisTimeout === currentTimeout) {
AntiClip.value = 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) { if (isOpen) {
on_open(window); on_open(window);
// 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 { else {
timeout(Number(transition_duration), () => { timeout(transition_duration, () => {
on_close(window); on_close(window);
}); });
overlay.css = `${css}
transition: margin
${transition_duration}ms ${bezier},
opacity
${transition_duration}ms ${bezier};
`;
} }
} }
}); });
}, },
child: child || Box(),
}), }),
}), }),
}); });
return window; return window;
}; };

View file

@ -61,5 +61,6 @@ export default () => PopupWindow({
close_on_unfocus: 'stay', close_on_unfocus: 'stay',
transition: 'slide_up', transition: 'slide_up',
transition_duration, transition_duration,
bezier: 'ease',
child: OSDs(), child: OSDs(),
}); });