nosignal-shell/utils/widgets.tsx
2025-01-14 12:57:10 +11:00

156 lines
5.1 KiB
TypeScript

import { Binding, property, register, timeout } from "astal";
import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
if (!text) return null;
const window = (
<window
visible={false}
namespace="tooltip"
keymode={Astal.Keymode.NONE}
exclusivity={Astal.Exclusivity.IGNORE}
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
>
<label className="tooltip" label={text} />
</window>
) as Widget.Window;
self.set_tooltip_window(window);
let dirty = true;
let lastX = 0;
self.connect("size-allocate", () => (dirty = true));
window.connect("size-allocate", () => {
window.marginLeft = lastX + (self.get_allocated_width() - window.get_preferred_width()[1]) / 2;
});
if (text instanceof Binding) self.hook(text, (_: any, v: string) => !v && window.hide());
self.connect("query-tooltip", (_: any, x: number, y: number) => {
if (text instanceof Binding && !text.get()) return false;
if (dirty) {
const { width, height } = self.get_allocation();
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
window.marginLeft = cx + ((width - window.get_preferred_width()[1]) / 2 - x);
window.marginTop = cy + (height - y);
lastX = cx - x;
dirty = false;
}
return true;
});
self.connect("destroy", () => window.destroy());
return window;
};
export const setupChildClickthrough = (self: any) =>
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
export enum TransitionType {
FADE = "",
SLIDE_DOWN = "margin-top: -${height}px; margin-bottom: ${height}px;",
SLIDE_UP = "margin-top: ${height}px; margin-bottom: -${height}px;",
SLIDE_RIGHT = "margin-left: -${width}px; margin-right: ${width}px;",
SLIDE_LEFT = "margin-left: ${width}px; margin-right: -${width}px;",
}
@register()
export class PopupWindow extends Widget.Window {
readonly transitionType: TransitionType;
readonly transitionDuration: number;
readonly transitionAmount: number;
readonly #content: Widget.Box;
#visible: boolean = false;
@property(Boolean)
get realVisible() {
return this.#visible;
}
set realVisible(v: boolean) {
if (v) this.show();
else this.hide();
}
constructor(
props: Widget.WindowProps & {
transitionType?: TransitionType;
transitionDuration?: number;
transitionAmount?: number;
}
) {
const {
clickThrough,
child,
halign = Gtk.Align.START,
valign = Gtk.Align.START,
transitionType = TransitionType.FADE,
transitionDuration = 200,
transitionAmount = 0.2,
...sProps
} = props;
sProps.visible = false;
sProps.application = App;
sProps.namespace = `caelestia-${props.name}`;
sProps.anchor =
Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT;
sProps.onKeyPressEvent = (self, event) => {
// Close window on escape
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
return props.onKeyPressEvent?.(self, event);
};
super(sProps);
this.transitionType = transitionType;
this.transitionDuration = transitionDuration;
this.transitionAmount = transitionAmount;
// Wrapper box for animations
this.#content = (
<box halign={halign} valign={valign} className={`${props.name}-wrapper`}>
{clickThrough ? <eventbox>{child}</eventbox> : child}
</box>
) as Widget.Box;
this.#content.css = this.#getTransitionCss(false);
this.add(this.#content);
if (clickThrough) setupChildClickthrough(this);
}
#getTransitionCss(visible: boolean) {
return (
`transition-duration: ${this.transitionDuration}ms;` +
(visible
? "opacity: 1;" + this.transitionType.replaceAll("${width}", "0").replaceAll("${height}", "0")
: "opacity: 0;" +
this.transitionType
.replaceAll("${width}", String(this.#content.get_preferred_width()[1] * this.transitionAmount))
.replaceAll("${height}", String(this.#content.get_preferred_height()[1] * this.transitionAmount)))
);
}
show() {
this.#visible = true;
this.notify("real-visible");
super.show();
this.#content.css = this.#getTransitionCss(true);
}
hide() {
this.#visible = false;
this.notify("real-visible");
this.#content.css = this.#getTransitionCss(false);
timeout(this.transitionDuration, () => !this.#visible && super.hide());
}
toggle() {
if (this.#visible) this.hide();
else this.show();
}
}