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 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");
|
||||
|
|
|
|||
|
|
@ -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(""));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)} />;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue