parent
5d27b3d975
commit
f3e06554e4
105 changed files with 245 additions and 254 deletions
nixosModules/ags/config/widgets/misc
29
nixosModules/ags/config/widgets/misc/_index.scss
Normal file
29
nixosModules/ags/config/widgets/misc/_index.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
@use '../../style/colors';
|
||||
|
||||
.sorted-list {
|
||||
.search {
|
||||
icon {
|
||||
font-size: 20px;
|
||||
min-width: 40px;
|
||||
min-height: 40px
|
||||
}
|
||||
|
||||
// entry {}
|
||||
}
|
||||
|
||||
.list {
|
||||
row {
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover, &:selected {
|
||||
icon {
|
||||
-gtk-icon-shadow: 2px 2px colors.$accent_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
111
nixosModules/ags/config/widgets/misc/popup-window.tsx
Normal file
111
nixosModules/ags/config/widgets/misc/popup-window.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { App, Astal, Gtk, Widget } from 'astal/gtk3';
|
||||
import { property, register } from 'astal/gobject';
|
||||
import { Binding, idle } from 'astal';
|
||||
|
||||
import { get_hyprland_monitor, hyprMessage } from '../../lib';
|
||||
|
||||
/* Types */
|
||||
type CloseType = 'none' | 'stay' | 'released' | 'clicked';
|
||||
type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' |
|
||||
'slide right' | 'popin' | 'fade';
|
||||
type PopupCallback = (self?: Widget.Window) => void;
|
||||
|
||||
export type PopupWindowProps = Widget.WindowProps & {
|
||||
transition?: HyprTransition | Binding<HyprTransition>
|
||||
close_on_unfocus?: CloseType | Binding<CloseType>
|
||||
on_open?: PopupCallback
|
||||
on_close?: PopupCallback
|
||||
};
|
||||
|
||||
|
||||
@register()
|
||||
export class PopupWindow extends Widget.Window {
|
||||
@property(String)
|
||||
declare transition: HyprTransition | Binding<HyprTransition>;
|
||||
|
||||
@property(String)
|
||||
declare close_on_unfocus: CloseType | Binding<CloseType>;
|
||||
|
||||
on_open: PopupCallback;
|
||||
on_close: PopupCallback;
|
||||
|
||||
constructor({
|
||||
transition = 'slide top',
|
||||
close_on_unfocus = 'released',
|
||||
on_open = () => { /**/ },
|
||||
on_close = () => { /**/ },
|
||||
|
||||
name,
|
||||
visible = false,
|
||||
layer = Astal.Layer.OVERLAY,
|
||||
...rest
|
||||
}: PopupWindowProps) {
|
||||
super({
|
||||
...rest,
|
||||
name: `win-${name}`,
|
||||
namespace: `win-${name}`,
|
||||
visible: false,
|
||||
layer,
|
||||
setup: () => idle(() => {
|
||||
// Add way to make window open on startup
|
||||
if (visible) {
|
||||
this.visible = true;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
App.add_window(this);
|
||||
|
||||
const setTransition = (_: PopupWindow, t: HyprTransition | Binding<HyprTransition>) => {
|
||||
hyprMessage(`keyword layerrule animation ${t}, ${this.name}`).catch(console.log);
|
||||
};
|
||||
|
||||
this.connect('notify::transition', setTransition);
|
||||
|
||||
this.close_on_unfocus = close_on_unfocus;
|
||||
this.transition = transition;
|
||||
this.on_open = on_open;
|
||||
this.on_close = on_close;
|
||||
|
||||
this.connect('notify::visible', () => {
|
||||
// Make sure we have the right animation
|
||||
setTransition(this, this.transition);
|
||||
|
||||
if (this.visible) {
|
||||
this.on_open(this);
|
||||
}
|
||||
else {
|
||||
this.on_close(this);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async set_x_pos(
|
||||
alloc: Gtk.Allocation,
|
||||
side = 'right' as 'left' | 'right',
|
||||
) {
|
||||
const monitor = this.gdkmonitor ??
|
||||
this.get_display().get_monitor_at_point(alloc.x, alloc.y);
|
||||
|
||||
const transform = get_hyprland_monitor(monitor)?.transform;
|
||||
|
||||
let width: number;
|
||||
|
||||
if (transform && (transform === 1 || transform === 3)) {
|
||||
width = monitor.get_geometry().height;
|
||||
}
|
||||
else {
|
||||
width = monitor.get_geometry().width;
|
||||
}
|
||||
|
||||
this.margin_right = side === 'right' ?
|
||||
(width - alloc.x - alloc.width) :
|
||||
this.margin_right;
|
||||
|
||||
this.margin_left = side === 'right' ?
|
||||
this.margin_left :
|
||||
(alloc.x - alloc.width);
|
||||
}
|
||||
}
|
||||
|
||||
export default PopupWindow;
|
14
nixosModules/ags/config/widgets/misc/separator.tsx
Normal file
14
nixosModules/ags/config/widgets/misc/separator.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Widget } from 'astal/gtk3';
|
||||
|
||||
|
||||
export default ({
|
||||
size,
|
||||
vertical = false,
|
||||
css = '',
|
||||
...rest
|
||||
}: { size: number } & Widget.BoxProps) => (
|
||||
<box
|
||||
css={`${vertical ? 'min-height' : 'min-width'}: ${size}px; ${css}`}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
61
nixosModules/ags/config/widgets/misc/smooth-progress.tsx
Normal file
61
nixosModules/ags/config/widgets/misc/smooth-progress.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { bind } from 'astal';
|
||||
import { Gtk, Widget } from 'astal/gtk3';
|
||||
import { register, property } from 'astal/gobject';
|
||||
|
||||
type SmoothProgressProps = Widget.BoxProps & {
|
||||
transition_duration?: string
|
||||
};
|
||||
|
||||
|
||||
// PERF: this is kinda laggy
|
||||
@register()
|
||||
class SmoothProgress extends Widget.Box {
|
||||
@property(Number)
|
||||
declare fraction: number;
|
||||
|
||||
@property(String)
|
||||
declare transition_duration: string;
|
||||
|
||||
constructor({
|
||||
transition_duration = '1s',
|
||||
...rest
|
||||
}: SmoothProgressProps = {}) {
|
||||
super(rest);
|
||||
this.transition_duration = transition_duration;
|
||||
|
||||
const background = (
|
||||
<box
|
||||
className="background"
|
||||
hexpand
|
||||
vexpand
|
||||
halign={Gtk.Align.FILL}
|
||||
valign={Gtk.Align.FILL}
|
||||
/>
|
||||
);
|
||||
|
||||
const progress = (
|
||||
<box
|
||||
className="progress"
|
||||
vexpand
|
||||
valign={Gtk.Align.FILL}
|
||||
css={bind(this, 'fraction').as((fraction) => {
|
||||
return `
|
||||
transition: margin-right ${this.transition_duration} linear;
|
||||
margin-right: ${
|
||||
Math.abs(fraction - 1) * background.get_allocated_width()
|
||||
}px;
|
||||
`;
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
this.add((
|
||||
<overlay overlay={progress}>
|
||||
{background}
|
||||
</overlay>
|
||||
));
|
||||
this.show_all();
|
||||
}
|
||||
}
|
||||
|
||||
export default SmoothProgress;
|
198
nixosModules/ags/config/widgets/misc/sorted-list.tsx
Normal file
198
nixosModules/ags/config/widgets/misc/sorted-list.tsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
// This is definitely not good practice but I couldn't figure out how to extend PopupWindow
|
||||
// so here we are with a cursed function that returns a prop of the class.
|
||||
|
||||
import { Astal, Gtk, Widget } from 'astal/gtk3';
|
||||
import { idle } from 'astal';
|
||||
|
||||
import { AsyncFzf, AsyncFzfOptions, FzfResultItem } from 'fzf';
|
||||
|
||||
import PopupWindow from '../misc/popup-window';
|
||||
import { centerCursor } from '../../lib';
|
||||
|
||||
export interface SortedListProps<T> {
|
||||
create_list: () => T[] | Promise<T[]>
|
||||
create_row: (item: T) => Gtk.Widget
|
||||
fzf_options?: AsyncFzfOptions<T>
|
||||
unique_props?: (keyof T)[]
|
||||
on_row_activated: (row: Gtk.ListBoxRow) => void
|
||||
sort_func: (
|
||||
a: Gtk.ListBoxRow,
|
||||
b: Gtk.ListBoxRow,
|
||||
entry: Widget.Entry,
|
||||
fzf: FzfResultItem<T>[],
|
||||
) => number
|
||||
name: string
|
||||
};
|
||||
|
||||
|
||||
export class SortedList<T> {
|
||||
private item_list: T[] = [];
|
||||
private fzf_results: FzfResultItem<T>[] = [];
|
||||
|
||||
readonly window: PopupWindow;
|
||||
private _item_map = new Map<T, Gtk.Widget>();
|
||||
|
||||
readonly create_list: () => T[] | Promise<T[]>;
|
||||
readonly create_row: (item: T) => Gtk.Widget;
|
||||
readonly fzf_options: AsyncFzfOptions<T>;
|
||||
readonly unique_props: (keyof T)[] | undefined;
|
||||
|
||||
readonly on_row_activated: (row: Gtk.ListBoxRow) => void;
|
||||
|
||||
readonly sort_func: (
|
||||
a: Gtk.ListBoxRow,
|
||||
b: Gtk.ListBoxRow,
|
||||
entry: Widget.Entry,
|
||||
fzf: FzfResultItem<T>[],
|
||||
) => number;
|
||||
|
||||
|
||||
constructor({
|
||||
create_list,
|
||||
create_row,
|
||||
fzf_options = {} as AsyncFzfOptions<T>,
|
||||
unique_props,
|
||||
on_row_activated,
|
||||
sort_func,
|
||||
name,
|
||||
}: SortedListProps<T>) {
|
||||
const list = new Gtk.ListBox({
|
||||
selectionMode: Gtk.SelectionMode.SINGLE,
|
||||
});
|
||||
|
||||
list.connect('row-activated', (_, row) => {
|
||||
this.on_row_activated(row);
|
||||
});
|
||||
|
||||
const placeholder = (
|
||||
<revealer>
|
||||
<label
|
||||
label=" Couldn't find a match"
|
||||
className="placeholder"
|
||||
/>
|
||||
</revealer>
|
||||
) as Widget.Revealer;
|
||||
|
||||
const on_text_change = (text: string) => {
|
||||
// @ts-expect-error this works
|
||||
(new AsyncFzf<T[]>(this.item_list, this.fzf_options)).find(text)
|
||||
.then((out) => {
|
||||
this.fzf_results = out;
|
||||
list.invalidate_sort();
|
||||
|
||||
const visibleApplications = list.get_children().filter((row) => row.visible).length;
|
||||
|
||||
placeholder.reveal_child = visibleApplications <= 0;
|
||||
})
|
||||
.catch(() => { /**/ });
|
||||
};
|
||||
|
||||
const entry = (
|
||||
<entry
|
||||
onChanged={(self) => on_text_change(self.text)}
|
||||
hexpand
|
||||
/>
|
||||
) as Widget.Entry;
|
||||
|
||||
list.set_sort_func((a, b) => {
|
||||
return this.sort_func(a, b, entry, this.fzf_results);
|
||||
});
|
||||
|
||||
const refreshItems = () => idle(async() => {
|
||||
// Delete items that don't exist anymore
|
||||
const new_list = await this.create_list();
|
||||
|
||||
[...this._item_map].forEach(([item, widget]) => {
|
||||
if (!new_list.some((new_item) =>
|
||||
this.unique_props?.every((prop) => item[prop] === new_item[prop]) ??
|
||||
item === new_item)) {
|
||||
widget.get_parent()?.destroy();
|
||||
this._item_map.delete(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Add missing items
|
||||
new_list.forEach((item) => {
|
||||
if (!this.item_list.some((old_item) =>
|
||||
this.unique_props?.every((prop) => old_item[prop] === item[prop]) ??
|
||||
old_item === item)) {
|
||||
const itemWidget = this.create_row(item);
|
||||
|
||||
list.add(itemWidget);
|
||||
this._item_map.set(item, itemWidget);
|
||||
}
|
||||
});
|
||||
|
||||
this.item_list = new_list;
|
||||
|
||||
list.show_all();
|
||||
on_text_change('');
|
||||
});
|
||||
|
||||
this.window = (
|
||||
<PopupWindow
|
||||
name={name}
|
||||
keymode={Astal.Keymode.ON_DEMAND}
|
||||
on_open={() => {
|
||||
entry.text = '';
|
||||
refreshItems();
|
||||
centerCursor();
|
||||
entry.grab_focus();
|
||||
}}
|
||||
>
|
||||
<box
|
||||
vertical
|
||||
className={`${name} sorted-list`}
|
||||
>
|
||||
<box className="widget search">
|
||||
|
||||
<icon icon="preferences-system-search-symbolic" />
|
||||
|
||||
{entry}
|
||||
|
||||
<button
|
||||
css="margin-left: 5px;"
|
||||
cursor="pointer"
|
||||
onButtonReleaseEvent={refreshItems}
|
||||
>
|
||||
<icon icon="view-refresh-symbolic" css="font-size: 26px;" />
|
||||
</button>
|
||||
|
||||
</box>
|
||||
|
||||
<eventbox cursor="pointer">
|
||||
<scrollable
|
||||
className="widget list"
|
||||
|
||||
css="min-height: 600px; min-width: 700px;"
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<box vertical>
|
||||
{list}
|
||||
{placeholder}
|
||||
</box>
|
||||
</scrollable>
|
||||
</eventbox>
|
||||
</box>
|
||||
</PopupWindow>
|
||||
) as PopupWindow;
|
||||
|
||||
this.create_list = create_list;
|
||||
this.create_row = create_row;
|
||||
this.fzf_options = fzf_options;
|
||||
this.unique_props = unique_props;
|
||||
this.on_row_activated = on_row_activated;
|
||||
this.sort_func = sort_func;
|
||||
|
||||
refreshItems();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param props props for a SortedList Widget
|
||||
* @returns the widget
|
||||
*/
|
||||
export default function<Attr>(props: SortedListProps<Attr>) {
|
||||
return (new SortedList(props)).window;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue