feat(ags popup): allow setting custom bezier for windows
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
60615feeb9
commit
111a065293
2 changed files with 321 additions and 65 deletions
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue