nixos-configs/modules/ags/config/widgets/notifs/gesture.tsx

360 lines
11 KiB
TypeScript
Raw Normal View History

import { Gdk, Gtk, Widget } from 'astal/gtk3';
2024-10-22 13:09:39 -04:00
import { property, register } from 'astal/gobject';
2024-10-29 17:30:40 -04:00
import { idle, interval, timeout } from 'astal';
2024-10-22 13:09:39 -04:00
import AstalIO from 'gi://AstalIO';
2024-10-22 13:09:39 -04:00
import AstalNotifd from 'gi://AstalNotifd';
import { hyprMessage } from '../../lib';
2024-10-15 23:56:11 -04:00
import { HasNotifs } from './notification';
2024-10-15 23:56:11 -04:00
import { get_hyprland_monitor } from '../../lib';
/* Types */
2024-10-16 21:44:45 -04:00
import { CursorPos, LayerResult } from '../../lib';
const display = Gdk.Display.get_default();
const MAX_OFFSET = 200;
const OFFSCREEN = 300;
const ANIM_DURATION = 500;
const SLIDE_MIN_THRESHOLD = 10;
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
const MAX_LEFT = `
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const MAX_RIGHT = `
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const slideLeft = `${TRANSITION} ${MAX_LEFT} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
type NotifGestureWrapperProps = Widget.BoxProps & {
id: number
slide_in_from?: 'Left' | 'Right'
popup_timer?: number
setup_notif?: (self: NotifGestureWrapper) => void
};
@register()
2024-10-15 23:56:11 -04:00
export class NotifGestureWrapper extends Widget.EventBox {
2024-10-22 13:09:39 -04:00
public static popups = new Map<number, NotifGestureWrapper>();
public static sliding_in = 0;
public static on_sliding_in: (amount: number) => void;
2024-10-15 23:56:11 -04:00
readonly id: number;
readonly slide_in_from: 'Left' | 'Right';
2024-10-16 23:56:05 -04:00
readonly is_popup: boolean;
private timer_object: AstalIO.Time | undefined;
2024-10-22 13:09:39 -04:00
@property(Number)
declare popup_timer: number;
2024-10-22 13:09:39 -04:00
@property(Boolean)
declare dragging: boolean;
2024-10-16 13:00:10 -04:00
2024-10-29 17:30:40 -04:00
private _sliding_away = false;
2024-10-22 13:09:39 -04:00
private async get_hovered(): Promise<boolean> {
const layers = JSON.parse(await hyprMessage('j/layers')) as LayerResult;
const cursorPos = JSON.parse(await hyprMessage('j/cursorpos')) as CursorPos;
2024-10-22 13:09:39 -04:00
const win = this.get_window();
2024-10-22 13:09:39 -04:00
if (!win) {
return false;
}
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
const monitor = display?.get_monitor_at_window(win);
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
if (!monitor) {
return false;
}
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
const plugName = get_hyprland_monitor(monitor)?.name;
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
const notifLayer = layers[plugName ?? '']?.levels['3']
?.find((n) => n.namespace === 'notifications');
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
if (!notifLayer) {
return false;
}
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
const index = [...NotifGestureWrapper.popups.keys()]
.sort((a, b) => b - a)
.indexOf(this.id);
2024-10-16 22:33:15 -04:00
2024-10-22 13:09:39 -04:00
const popups = [...NotifGestureWrapper.popups.entries()]
.sort((a, b) => b[0] - a[0])
.map(([key, val]) => [key, val.get_allocated_height()]);
const thisY = notifLayer.y + popups
.map((v) => v[1])
.slice(0, index)
.reduce((prev, curr) => prev + curr, 0);
2024-10-29 17:30:40 -04:00
if (cursorPos.y >= thisY && cursorPos.y <= thisY + (popups[index]?.at(1) ?? 0)) {
2024-10-22 13:09:39 -04:00
if (cursorPos.x >= notifLayer.x &&
cursorPos.x <= notifLayer.x + notifLayer.w) {
return true;
2024-10-15 23:56:11 -04:00
}
}
return false;
}
2024-10-22 13:09:39 -04:00
private setCursor(cursor: string) {
if (!display) {
return;
}
this.window.set_cursor(Gdk.Cursor.new_from_name(
display,
cursor,
));
}
public slideAway(side: 'Left' | 'Right', duplicate = false): void {
2024-10-29 17:30:40 -04:00
if (!this.sensitive || this._sliding_away) {
2024-10-16 23:56:05 -04:00
return;
}
2024-10-15 23:56:11 -04:00
// Make it uninteractable
this.sensitive = false;
2024-10-29 17:30:40 -04:00
this._sliding_away = true;
let rev = this.get_child() as Widget.Revealer | null;
if (!rev) {
return;
}
const revChild = rev.get_child() as Widget.Box | null;
if (!revChild) {
return;
}
revChild.css = side === 'Left' ? slideLeft : slideRight;
timeout(ANIM_DURATION - 100, () => {
rev = this.get_child() as Widget.Revealer | null;
if (!rev) {
return;
}
2024-10-15 23:56:11 -04:00
2024-10-29 17:30:40 -04:00
rev.revealChild = false;
2024-10-15 23:56:11 -04:00
2024-10-29 17:30:40 -04:00
timeout(ANIM_DURATION, () => {
if (!duplicate) {
// Kill notif if specified
if (!this.is_popup) {
const notifications = AstalNotifd.get_default();
notifications.get_notification(this.id)?.dismiss();
// Update HasNotifs
HasNotifs.set(notifications.get_notifications().length > 0);
}
else {
// Make sure we cleanup any references to this instance
NotifGestureWrapper.popups.delete(this.id);
}
2024-10-16 23:56:05 -04:00
}
// Get rid of disappeared widget
2024-10-15 23:56:11 -04:00
this.destroy();
2024-10-29 17:30:40 -04:00
});
});
2024-10-15 23:56:11 -04:00
}
constructor({
id,
slide_in_from = 'Left',
popup_timer = 0,
setup_notif = () => { /**/ },
...rest
}: NotifGestureWrapperProps) {
const notifications = AstalNotifd.get_default();
super({
on_button_press_event: () => {
this.setCursor('grabbing');
},
// OnRelease
on_button_release_event: () => {
this.setCursor('grab');
},
// OnHover
on_enter_notify_event: () => {
this.setCursor('grab');
},
// OnHoverLost
on_leave_notify_event: () => {
this.setCursor('grab');
},
2024-10-29 17:30:40 -04:00
onDestroy: () => {
this.timer_object?.cancel();
},
});
this.id = id;
this.slide_in_from = slide_in_from;
this.dragging = false;
2024-10-16 13:00:10 -04:00
this.popup_timer = popup_timer;
2024-10-16 23:56:05 -04:00
this.is_popup = this.popup_timer !== 0;
2024-10-16 13:00:10 -04:00
// Handle timeout before sliding away if it is a popup
if (this.popup_timer !== 0) {
this.timer_object = interval(1000, async() => {
try {
if (!(await this.get_hovered())) {
if (this.popup_timer === 0) {
this.slideAway('Left');
}
else {
--this.popup_timer;
}
}
}
2024-10-29 17:30:40 -04:00
catch (_e) {
this.timer_object?.cancel();
}
});
}
this.hook(notifications, 'notified', (_, notifId) => {
if (notifId === this.id) {
this.slideAway(this.is_popup ? 'Left' : 'Right', true);
}
});
const gesture = Gtk.GestureDrag.new(this);
this.add(
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={500}
revealChild={false}
>
<box
{...rest}
setup={(self) => {
self
// When dragging
.hook(gesture, 'drag-update', () => {
let offset = gesture.get_offset()[1];
if (!offset || offset === 0) {
return;
}
// Slide right
if (offset > 0) {
self.css = `
opacity: 1; transition: none;
margin-left: ${offset}px;
margin-right: -${offset}px;
`;
}
// Slide left
else {
offset = Math.abs(offset);
self.css = `
opacity: 1; transition: none;
margin-right: ${offset}px;
margin-left: -${offset}px;
`;
}
// Put a threshold on if a click is actually dragging
this.dragging = Math.abs(offset) > SLIDE_MIN_THRESHOLD;
2024-10-22 13:09:39 -04:00
this.setCursor('grabbing');
})
// On drag end
.hook(gesture, 'drag-end', () => {
const offset = gesture.get_offset()[1];
if (!offset) {
return;
}
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
2024-10-22 13:09:39 -04:00
this.slideAway(offset > 0 ? 'Right' : 'Left');
}
else {
self.css = defaultStyle;
this.dragging = false;
2024-10-22 13:09:39 -04:00
this.setCursor('grab');
}
});
if (this.is_popup) {
NotifGestureWrapper.on_sliding_in(++NotifGestureWrapper.sliding_in);
}
// Reverse of slideAway, so it started at squeeze, then we go to slide
self.css = this.slide_in_from === 'Left' ?
slideLeft :
slideRight;
idle(() => {
if (!notifications.get_notification(id)) {
2024-10-29 17:30:40 -04:00
return;
}
const rev = self?.get_parent() as Widget.Revealer | null;
if (!rev) {
return;
}
rev.revealChild = true;
timeout(ANIM_DURATION, () => {
if (!notifications.get_notification(id)) {
2024-10-29 17:30:40 -04:00
return;
}
// Then we go to center
self.css = defaultStyle;
if (this.is_popup) {
2024-10-29 17:30:40 -04:00
timeout(ANIM_DURATION, () => {
NotifGestureWrapper.on_sliding_in(
--NotifGestureWrapper.sliding_in,
);
2024-10-29 17:30:40 -04:00
});
}
2024-10-29 17:30:40 -04:00
});
});
}}
/>
</revealer>,
);
2024-10-16 23:56:05 -04:00
setup_notif(this);
}
}
export default NotifGestureWrapper;