better popup window
Not manual using css, instead use a combination of css opacity and Hyprland animations
This commit is contained in:
parent
f1298fbb29
commit
dcd914dbae
5 changed files with 61 additions and 156 deletions
9
app.tsx
9
app.tsx
|
|
@ -5,7 +5,6 @@ import Bar from "./modules/bar";
|
||||||
import Launcher from "./modules/launcher";
|
import Launcher from "./modules/launcher";
|
||||||
import NotifPopups from "./modules/notifpopups";
|
import NotifPopups from "./modules/notifpopups";
|
||||||
import Players from "./services/players";
|
import Players from "./services/players";
|
||||||
import { PopupWindow } from "./utils/widgets";
|
|
||||||
|
|
||||||
const loadStyleAsync = async () => {
|
const loadStyleAsync = async () => {
|
||||||
if (!GLib.file_test(`${SRC}/scss/scheme/_index.scss`, GLib.FileTest.EXISTS))
|
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 next") Players.get_default().lastPlayer?.next();
|
||||||
else if (request === "media previous") Players.get_default().lastPlayer?.previous();
|
else if (request === "media previous") Players.get_default().lastPlayer?.previous();
|
||||||
else if (request === "media stop") Players.get_default().lastPlayer?.stop();
|
else if (request === "media stop") Players.get_default().lastPlayer?.stop();
|
||||||
else if (request.startsWith("toggle")) {
|
else return res("Unknown command: " + request);
|
||||||
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);
|
|
||||||
|
|
||||||
if (log) console.log(`Request handled: ${request}`);
|
if (log) console.log(`Request handled: ${request}`);
|
||||||
res("OK");
|
res("OK");
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Apps } from "../services/apps";
|
||||||
import Math, { type HistoryItem } from "../services/math";
|
import Math, { type HistoryItem } from "../services/math";
|
||||||
import { getAppCategoryIcon } from "../utils/icons";
|
import { getAppCategoryIcon } from "../utils/icons";
|
||||||
import { launch } from "../utils/system";
|
import { launch } from "../utils/system";
|
||||||
import { PopupWindow, setupCustomTooltip, TransitionType } from "../utils/widgets";
|
import { convertPopupWindowProps, setupCustomTooltip } from "../utils/widgets";
|
||||||
|
|
||||||
type Mode = "apps" | "files" | "math";
|
type Mode = "apps" | "files" | "math";
|
||||||
|
|
||||||
|
|
@ -323,11 +323,7 @@ const LauncherContent = ({
|
||||||
showResults: Variable<boolean>;
|
showResults: Variable<boolean>;
|
||||||
entry: Widget.Entry;
|
entry: Widget.Entry;
|
||||||
}) => (
|
}) => (
|
||||||
<box
|
<box vertical className={bind(mode).as(m => `launcher ${m}`)}>
|
||||||
vertical
|
|
||||||
className={bind(mode).as(m => `launcher ${m}`)}
|
|
||||||
css={bind(AstalHyprland.get_default(), "focusedMonitor").as(m => `margin-top: ${m.height / 4}px;`)}
|
|
||||||
>
|
|
||||||
<box className="search-bar">
|
<box className="search-bar">
|
||||||
<label className="icon" label="search" />
|
<label className="icon" label="search" />
|
||||||
<SearchEntry entry={entry} />
|
<SearchEntry entry={entry} />
|
||||||
|
|
@ -351,7 +347,7 @@ const LauncherContent = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
export default class Launcher extends PopupWindow {
|
export default class Launcher extends Widget.Window {
|
||||||
readonly mode: Variable<Mode>;
|
readonly mode: Variable<Mode>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -359,8 +355,10 @@ export default class Launcher extends PopupWindow {
|
||||||
const mode = Variable<Mode>("apps");
|
const mode = Variable<Mode>("apps");
|
||||||
const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
|
const showResults = Variable.derive([bind(entry, "textLength"), mode], (t, m) => t > 0 || m !== "apps");
|
||||||
|
|
||||||
super({
|
super(
|
||||||
|
convertPopupWindowProps({
|
||||||
name: "launcher",
|
name: "launcher",
|
||||||
|
anchor: Astal.WindowAnchor.TOP,
|
||||||
keymode: Astal.Keymode.EXCLUSIVE,
|
keymode: Astal.Keymode.EXCLUSIVE,
|
||||||
onKeyPressEvent(_, event) {
|
onKeyPressEvent(_, event) {
|
||||||
const keyval = event.get_keyval()[1];
|
const keyval = event.get_keyval()[1];
|
||||||
|
|
@ -374,14 +372,16 @@ export default class Launcher extends PopupWindow {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transitionType: TransitionType.SLIDE_DOWN,
|
|
||||||
halign: Gtk.Align.CENTER,
|
halign: Gtk.Align.CENTER,
|
||||||
valign: Gtk.Align.START,
|
valign: Gtk.Align.START,
|
||||||
child: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
|
child: <LauncherContent mode={mode} showResults={showResults} entry={entry} />,
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
|
|
||||||
|
this.connect("show", () => (this.marginTop = AstalHyprland.get_default().focusedMonitor.height / 4));
|
||||||
|
|
||||||
// Clear search on hide if not in math mode
|
// Clear search on hide if not in math mode
|
||||||
this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
|
this.connect("hide", () => mode.get() !== "math" && entry.set_text(""));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-wrapper {
|
|
||||||
@include lib.ease-in-out;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
@include lib.overshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher {
|
.launcher {
|
||||||
@include lib.rounded(10);
|
@include lib.rounded(10);
|
||||||
@include lib.border(scheme.$overlay0, 0.2);
|
@include lib.border(scheme.$overlay0, 0.2);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@ label.icon {
|
||||||
@include font.icon;
|
@include font.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.popup {
|
||||||
|
transition: opacity 300ms linear;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
separator,
|
separator,
|
||||||
.separator {
|
.separator {
|
||||||
@include lib.rounded(2);
|
@include lib.rounded(2);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Binding, property, register, timeout } from "astal";
|
import { Binding } from "astal";
|
||||||
import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
|
import { App, Astal, Gdk, Widget } from "astal/gtk3";
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
|
||||||
export const setupCustomTooltip = (self: any, text: string | Binding<string>) => {
|
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) =>
|
export const setupChildClickthrough = (self: any) =>
|
||||||
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
|
self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes());
|
||||||
|
|
||||||
export enum TransitionType {
|
const overrideProp = <T,>(
|
||||||
FADE = "",
|
prop: T | Binding<T | undefined> | undefined,
|
||||||
SLIDE_DOWN = "margin-top: -${height}px; margin-bottom: ${height}px;",
|
override: (prop: T | undefined) => T | undefined
|
||||||
SLIDE_UP = "margin-top: ${height}px; margin-bottom: -${height}px;",
|
) => prop && (prop instanceof Binding ? prop.as(override) : override(prop));
|
||||||
SLIDE_RIGHT = "margin-left: -${width}px; margin-right: ${width}px;",
|
|
||||||
SLIDE_LEFT = "margin-left: ${width}px; margin-right: -${width}px;",
|
|
||||||
}
|
|
||||||
|
|
||||||
@register()
|
export const convertPopupWindowProps = (props: Widget.WindowProps): Widget.WindowProps => ({
|
||||||
export class PopupWindow extends Widget.Window {
|
keymode: Astal.Keymode.ON_DEMAND,
|
||||||
readonly transitionType: TransitionType;
|
exclusivity: Astal.Exclusivity.IGNORE,
|
||||||
readonly transitionInDuration: number;
|
...props,
|
||||||
readonly transitionOutDuration: number;
|
visible: false,
|
||||||
readonly transitionAmount: number;
|
application: App,
|
||||||
|
namespace: overrideProp(props.name, n => `caelestia-${n}`),
|
||||||
readonly #content: Widget.Box;
|
className: overrideProp(props.className, c => `popup ${c}`),
|
||||||
#visible: boolean = false;
|
onKeyPressEvent: (self, event) => {
|
||||||
|
|
||||||
@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
|
// Close window on escape
|
||||||
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
|
if (event.get_keyval()[1] === Gdk.KEY_Escape) self.hide();
|
||||||
|
|
||||||
return props.onKeyPressEvent?.(self, event);
|
return props.onKeyPressEvent?.(self, event);
|
||||||
};
|
},
|
||||||
super(sProps);
|
setup: self => {
|
||||||
|
self.connect("notify::visible", () => self.toggleClassName("visible", self.visible));
|
||||||
|
props.setup?.(self);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.transitionType = transitionType;
|
export const PopupWindow = (props: Widget.WindowProps) => <window {...convertPopupWindowProps(props)} />;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue