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) => { if (!text) return null; const 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 transitionInDuration: number; readonly transitionOutDuration: 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; transitionInDuration?: number; transitionOutDuration?: number; transitionAmount?: number; } ) { const { clickThrough, child, halign = Gtk.Align.START, valign = Gtk.Align.START, transitionType = TransitionType.FADE, transitionInDuration = 300, transitionOutDuration = 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.exclusivity = Astal.Exclusivity.IGNORE; if (!sProps.keymode) sProps.keymode = Astal.Keymode.ON_DEMAND; 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.transitionInDuration = transitionInDuration; this.transitionOutDuration = transitionOutDuration; this.transitionAmount = transitionAmount; // Wrapper box for animations this.#content = ( {clickThrough ? {child} : child} ) as Widget.Box; this.#content.css = this.#getTransitionCss(false); this.add(this.#content); if (clickThrough) setupChildClickthrough(this); } #getTransitionCss(visible: boolean) { return ( `transition-duration: ${visible ? this.transitionInDuration : this.transitionOutDuration}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.toggleClassName("visible", true); this.#content.css = this.#getTransitionCss(true); } hide() { this.#visible = false; this.notify("real-visible"); this.#content.toggleClassName("visible", false); this.#content.css = this.#getTransitionCss(false); timeout(this.transitionOutDuration, () => !this.#visible && super.hide()); } toggle() { if (this.#visible) this.hide(); else this.show(); } }