better popup window

Not manual using css, instead use a combination of css opacity and Hyprland animations
This commit is contained in:
2 * r + 2 * t 2025-01-15 15:22:22 +11:00
parent f1298fbb29
commit dcd914dbae
5 changed files with 61 additions and 156 deletions

View file

@ -5,7 +5,6 @@ import Bar from "./modules/bar";
import Launcher from "./modules/launcher";
import NotifPopups from "./modules/notifpopups";
import Players from "./services/players";
import { PopupWindow } from "./utils/widgets";
const loadStyleAsync = async () => {
if (!GLib.file_test(`${SRC}/scss/scheme/_index.scss`, GLib.FileTest.EXISTS))
@ -34,13 +33,7 @@ App.start({
else if (request === "media next") Players.get_default().lastPlayer?.next();
else if (request === "media previous") Players.get_default().lastPlayer?.previous();
else if (request === "media stop") Players.get_default().lastPlayer?.stop();
else if (request.startsWith("toggle")) {
const window = App.get_window(request.slice(7));
if (window instanceof PopupWindow) window.toggle();
else App.toggle_window(request.slice(7));
log = false;
} else return res("Unknown command: " + request);
else return res("Unknown command: " + request);
if (log) console.log(`Request handled: ${request}`);
res("OK");

View file

@ -8,7 +8,7 @@ import { Apps } from "../services/apps";
import Math, { type HistoryItem } from "../services/math";
import { getAppCategoryIcon } from "../utils/icons";
import { launch } from "../utils/system";
import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets";
import { convertPopupWindowProps, setupCustomTooltip } from "../utils/widgets";
type Mode = "apps" | "files" | "math";
@ -323,11 +323,7 @@ const LauncherContent = ({
showResults: Variable<boolean>;
entry: Widget.Entry;
}) => (
<box
vertical
className={bind(mode).as(m => `launcher ${m}`)}
css={bind(AstalHyprland.get_default(), "focusedMonitor").as(m => `margin-top: ${m.height / 4}px;`)}
>
<box vertical className={bind(mode).as(m => `launcher ${m}`)}>
<box className="search-bar">
<label className="icon" label="search" />
<SearchEntry entry={entry} />
@ -351,7 +347,7 @@ const LauncherContent = ({
);
@register()
export default class Launcher extends PopupWindow {
export default class Launcher extends Widget.Window {
readonly mode: Variable<Mode>;
constructor() {
@ -359,29 +355,33 @@ export default class Launcher extends PopupWindow {
const mode = Variable<Mode>("apps");
const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
super({
name: "launcher",
keymode: Astal.Keymode.EXCLUSIVE,
onKeyPressEvent(_, event) {
const keyval = event.get_keyval()[1];
// Focus entry on typing
if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
entry.text += String.fromCharCode(keyval);
entry.grab_focus();
entry.set_position(-1);
super(
convertPopupWindowProps({
name: "launcher",
anchor: Astal.WindowAnchor.TOP,
keymode: Astal.Keymode.EXCLUSIVE,
onKeyPressEvent(_, event) {
const keyval = event.get_keyval()[1];
// Focus entry on typing
if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
entry.text += String.fromCharCode(keyval);
entry.grab_focus();
entry.set_position(-1);
// Consume event, if not consumed it will duplicate character in entry
return true;
}
},
transitionType: TransitionType.SLIDE_DOWN,
halign: Gtk.Align.CENTER,
valign: Gtk.Align.START,
child: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
});
// Consume event, if not consumed it will duplicate character in entry
return true;
}
},
halign: Gtk.Align.CENTER,
valign: Gtk.Align.START,
child: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
})
);
this.mode = mode;
this.connect("show", () => (this.marginTop = AstalHyprland.get_default().focusedMonitor.height / 4));
// Clear search on hide if not in math mode
this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
}

View file

@ -15,14 +15,6 @@
}
}
.launcher-wrapper {
@include lib.ease-in-out;
&.visible {
@include lib.overshot;
}
}
.launcher {
@include lib.rounded(10);
@include lib.border(scheme.$overlay0, 0.2);

View file

@ -17,6 +17,15 @@ label.icon {
@include font.icon;
}
window.popup {
transition: opacity 300ms linear;
opacity: 0;
&.visible {
opacity: 1;
}
}
separator,
.separator {
@include lib.rounded(2);

View file

@ -1,5 +1,5 @@
import { Binding, property, register, timeout } from "astal";
import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { Binding } from "astal";
import { App, Astal, Gdk, Widget } from "astal/gtk3";
import AstalHyprland from "gi://AstalHyprland";
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
@ -47,118 +47,29 @@ export const setupCustomTooltip = (self: any, text: string | Binding<string>) =>
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;",
}
const overrideProp = <T,>(
prop: T | Binding<T | undefined> | undefined,
override: (prop: T | undefined) => T | undefined
) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
@register()
export class PopupWindow extends Widget.Window {
readonly transitionType: TransitionType;
readonly transitionInDuration: number;
readonly transitionOutDuration: number;
readonly transitionAmount: number;
export const convertPopupWindowProps = (props: Widget.WindowProps): Widget.WindowProps => ({
keymode: Astal.Keymode.ON_DEMAND,
exclusivity: Astal.Exclusivity.IGNORE,
...props,
visible: false,
application: App,
namespace: overrideProp(props.name, n => `caelestia-${n}`),
className: overrideProp(props.className, c => `popup ${c}`),
onKeyPressEvent: (self, event) => {
// Close window on escape
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
readonly #content: Widget.Box;
#visible: boolean = false;
return props.onKeyPressEvent?.(self, event);
},
setup: self => {
self.connect("notify::visible", () => self.toggleClassName("visible", self.visible));
props.setup?.(self);
},
});
@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 = (
<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: ${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();
}
}
export const PopupWindow = (props: Widget.WindowProps) => <window {...convertPopupWindowProps(props)} />;