refactor(agsV2): abstract sorted-list code
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
ab6b00c259
commit
ccdde81356
8 changed files with 250 additions and 295 deletions
|
@ -17,6 +17,7 @@ window, viewport {
|
|||
@import 'widgets/date/style.scss';
|
||||
@import 'widgets/icon-browser/style.scss';
|
||||
@import 'widgets/lockscreen/style.scss';
|
||||
@import 'widgets/misc/style.scss';
|
||||
@import 'widgets/notifs/style.scss';
|
||||
@import 'widgets/powermenu/style.scss';
|
||||
@import 'widgets/screenshot/style.scss';
|
||||
|
|
|
@ -1,66 +1,36 @@
|
|||
import { App, Astal, Gtk, Widget } from 'astal/gtk3';
|
||||
import { idle } from 'astal';
|
||||
import { App } from 'astal/gtk3';
|
||||
|
||||
import AstalApps from 'gi://AstalApps';
|
||||
|
||||
import { Fzf, FzfResultItem } from 'fzf';
|
||||
import SortedList from '../misc/sorted-list';
|
||||
|
||||
import PopupWindow from '../misc/popup-window';
|
||||
import { centerCursor } from '../../lib';
|
||||
|
||||
import AppItemWidget, { AppItem } from './app-item';
|
||||
import { launchApp } from './launch';
|
||||
import AppItemWidget, { AppItem } from './app-item';
|
||||
|
||||
|
||||
export default () => {
|
||||
let Applications: AstalApps.Application[] = [];
|
||||
let fzfResults = [] as FzfResultItem<AstalApps.Application>[];
|
||||
export default () => SortedList({
|
||||
name: 'applauncher',
|
||||
|
||||
const list = new Gtk.ListBox({
|
||||
selectionMode: Gtk.SelectionMode.SINGLE,
|
||||
});
|
||||
create_list: () => AstalApps.Apps.new().get_list(),
|
||||
|
||||
list.connect('row-activated', (_, row) => {
|
||||
const app = (row.get_children()[0] as AppItem).app;
|
||||
create_row: (app) => AppItemWidget({ app }),
|
||||
|
||||
launchApp(app);
|
||||
App.get_window('win-applauncher')?.set_visible(false);
|
||||
});
|
||||
|
||||
const placeholder = (
|
||||
<revealer>
|
||||
<label
|
||||
label=" Couldn't find a match"
|
||||
className="placeholder"
|
||||
/>
|
||||
</revealer>
|
||||
) as Widget.Revealer;
|
||||
|
||||
const on_text_change = (text: string) => {
|
||||
const fzf = new Fzf(Applications, {
|
||||
fzf_options: {
|
||||
selector: (app) => app.name + app.executable,
|
||||
|
||||
tiebreakers: [
|
||||
(a, b) => b.item.frequency - a.item.frequency,
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
fzfResults = fzf.find(text);
|
||||
list.invalidate_sort();
|
||||
on_row_activated: (row) => {
|
||||
const app = (row.get_children()[0] as AppItem).app;
|
||||
|
||||
const visibleApplications = list.get_children().filter((row) => row.visible).length;
|
||||
launchApp(app);
|
||||
App.get_window('win-applauncher')?.set_visible(false);
|
||||
},
|
||||
|
||||
placeholder.reveal_child = visibleApplications <= 0;
|
||||
};
|
||||
|
||||
const entry = (
|
||||
<entry
|
||||
onChanged={(self) => on_text_change(self.text)}
|
||||
hexpand
|
||||
/>
|
||||
) as Widget.Entry;
|
||||
|
||||
list.set_sort_func((a, b) => {
|
||||
sort_func: (a, b, entry, fzfResults) => {
|
||||
const row1 = (a.get_children()[0] as AppItem).app;
|
||||
const row2 = (b.get_children()[0] as AppItem).app;
|
||||
|
||||
|
@ -79,72 +49,5 @@ export default () => {
|
|||
|
||||
return s2 - s1;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const refreshApplications = () => idle(() => {
|
||||
(list.get_children() as Gtk.ListBoxRow[])
|
||||
.forEach((child) => {
|
||||
child.destroy();
|
||||
});
|
||||
|
||||
Applications = AstalApps.Apps.new().get_list();
|
||||
|
||||
Applications
|
||||
.flatMap((app) => AppItemWidget({ app }))
|
||||
.forEach((child) => {
|
||||
list.add(child);
|
||||
});
|
||||
|
||||
list.show_all();
|
||||
on_text_change('');
|
||||
});
|
||||
|
||||
refreshApplications();
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
name="applauncher"
|
||||
keymode={Astal.Keymode.ON_DEMAND}
|
||||
on_open={() => {
|
||||
entry.text = '';
|
||||
centerCursor();
|
||||
}}
|
||||
>
|
||||
<box
|
||||
vertical
|
||||
className="applauncher"
|
||||
>
|
||||
<box className="widget app-search">
|
||||
|
||||
<icon icon="preferences-system-search-symbolic" />
|
||||
|
||||
{entry}
|
||||
|
||||
<button
|
||||
css="margin-left: 5px;"
|
||||
cursor="pointer"
|
||||
onButtonReleaseEvent={refreshApplications}
|
||||
>
|
||||
<icon icon="view-refresh-symbolic" css="font-size: 26px;" />
|
||||
</button>
|
||||
|
||||
</box>
|
||||
|
||||
<eventbox cursor="pointer">
|
||||
<scrollable
|
||||
className="widget app-list"
|
||||
|
||||
css="min-height: 600px; min-width: 600px;"
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<box vertical>
|
||||
{list}
|
||||
{placeholder}
|
||||
</box>
|
||||
</scrollable>
|
||||
</eventbox>
|
||||
</box>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,32 +1,6 @@
|
|||
.applauncher {
|
||||
.app-search {
|
||||
icon {
|
||||
font-size: 20px;
|
||||
min-width: 40px;
|
||||
min-height: 40px
|
||||
}
|
||||
|
||||
entry {}
|
||||
}
|
||||
|
||||
.app-list {
|
||||
row {
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover, &:selected {
|
||||
icon {
|
||||
-gtk-icon-shadow: 2px 2px $accent_color;
|
||||
}
|
||||
}
|
||||
|
||||
.app {
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,29 @@
|
|||
import { Astal, Gtk, Widget } from 'astal/gtk3';
|
||||
import { idle } from 'astal';
|
||||
import { Gtk, Widget } from 'astal/gtk3';
|
||||
|
||||
import { Fzf, FzfResultItem } from 'fzf';
|
||||
|
||||
import PopupWindow from '../misc/popup-window';
|
||||
import { centerCursor } from '../../lib';
|
||||
import SortedList from '../misc/sorted-list';
|
||||
|
||||
|
||||
export default () => {
|
||||
let Icons: string[] = [];
|
||||
let fzfResults = [] as FzfResultItem<string>[];
|
||||
export default () => SortedList({
|
||||
name: 'icon-browser',
|
||||
|
||||
const list = new Gtk.ListBox({
|
||||
selectionMode: Gtk.SelectionMode.SINGLE,
|
||||
});
|
||||
create_list: () => Gtk.IconTheme.get_default().list_icons(null)
|
||||
.filter((icon) => icon.endsWith('symbolic'))
|
||||
.sort(),
|
||||
|
||||
list.connect('row-activated', (_, row) => {
|
||||
create_row: (icon) => (
|
||||
<box>
|
||||
<icon css="font-size: 60px; margin-right: 25px;" icon={icon} />
|
||||
<label label={icon} />
|
||||
</box>
|
||||
),
|
||||
|
||||
on_row_activated: (row) => {
|
||||
const icon = ((row.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
|
||||
|
||||
console.log(icon);
|
||||
});
|
||||
},
|
||||
|
||||
const placeholder = (
|
||||
<revealer>
|
||||
<label
|
||||
label=" Couldn't find a match"
|
||||
className="placeholder"
|
||||
/>
|
||||
</revealer>
|
||||
) as Widget.Revealer;
|
||||
|
||||
const on_text_change = (text: string) => {
|
||||
const fzf = new Fzf(Icons);
|
||||
|
||||
fzfResults = fzf.find(text);
|
||||
list.invalidate_sort();
|
||||
|
||||
const visibleIcons = list.get_children().filter((row) => row.visible).length;
|
||||
|
||||
placeholder.reveal_child = visibleIcons <= 0;
|
||||
};
|
||||
|
||||
const entry = (
|
||||
<entry
|
||||
onChanged={(self) => on_text_change(self.text)}
|
||||
hexpand
|
||||
/>
|
||||
) as Widget.Entry;
|
||||
|
||||
list.set_sort_func((a, b) => {
|
||||
sort_func: (a, b, entry, fzfResults) => {
|
||||
const row1 = ((a.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
|
||||
const row2 = ((b.get_children()[0] as Widget.Box).get_children()[0] as Widget.Icon).icon;
|
||||
|
||||
|
@ -67,76 +42,5 @@ export default () => {
|
|||
|
||||
return s2 - s1;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const refreshIcons = () => idle(() => {
|
||||
(list.get_children() as Gtk.ListBoxRow[])
|
||||
.forEach((child) => {
|
||||
child.destroy();
|
||||
});
|
||||
|
||||
Icons = Gtk.IconTheme.get_default().list_icons(null)
|
||||
.filter((icon) => icon.endsWith('symbolic'))
|
||||
.sort();
|
||||
|
||||
Icons
|
||||
.flatMap((icon) => (
|
||||
<box>
|
||||
<icon css="font-size: 60px; margin-right: 25px;" icon={icon} />
|
||||
<label label={icon} />
|
||||
</box>
|
||||
))
|
||||
.forEach((child) => {
|
||||
list.add(child);
|
||||
});
|
||||
|
||||
list.show_all();
|
||||
on_text_change('');
|
||||
});
|
||||
|
||||
refreshIcons();
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
name="icon-browser"
|
||||
keymode={Astal.Keymode.ON_DEMAND}
|
||||
on_open={() => {
|
||||
entry.text = '';
|
||||
centerCursor();
|
||||
}}
|
||||
>
|
||||
<box
|
||||
vertical
|
||||
className="icon-browser"
|
||||
>
|
||||
<box className="widget icon-search">
|
||||
|
||||
<icon icon="preferences-system-search-symbolic" />
|
||||
|
||||
{entry}
|
||||
|
||||
<button
|
||||
css="margin-left: 5px;"
|
||||
onButtonReleaseEvent={refreshIcons}
|
||||
>
|
||||
<icon icon="view-refresh-symbolic" css="font-size: 26px;" />
|
||||
</button>
|
||||
|
||||
</box>
|
||||
|
||||
<scrollable
|
||||
className="widget icon-list"
|
||||
|
||||
css="min-height: 600px; min-width: 600px;"
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<box vertical>
|
||||
{list}
|
||||
{placeholder}
|
||||
</box>
|
||||
</scrollable>
|
||||
</box>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,32 +1,4 @@
|
|||
.icon-browser {
|
||||
.icon-search {
|
||||
icon {
|
||||
font-size: 20px;
|
||||
min-width: 40px;
|
||||
min-height: 40px
|
||||
}
|
||||
|
||||
entry {}
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
row {
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover, &:selected {
|
||||
icon {
|
||||
-gtk-icon-shadow: 2px 2px $accent_color;
|
||||
}
|
||||
}
|
||||
|
||||
box {
|
||||
.icon-browser .icon-list row box {
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ import { get_hyprland_monitor, hyprMessage } from '../../lib';
|
|||
type CloseType = 'none' | 'stay' | 'released' | 'clicked';
|
||||
type HyprTransition = 'slide' | 'slide top' | 'slide bottom' | 'slide left' |
|
||||
'slide right' | 'popin' | 'fade';
|
||||
type PopupCallback = (self: PopupWindow) => void;
|
||||
type PopupCallback = (self?: Widget.Window) => void;
|
||||
|
||||
type PopupWindowProps = Widget.WindowProps & {
|
||||
export type PopupWindowProps = Widget.WindowProps & {
|
||||
transition?: HyprTransition | Binding<HyprTransition>
|
||||
close_on_unfocus?: CloseType | Binding<CloseType>
|
||||
on_open?: PopupCallback
|
||||
|
|
174
nixosModules/ags/v2/widgets/misc/sorted-list.tsx
Normal file
174
nixosModules/ags/v2/widgets/misc/sorted-list.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
// 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 { Fzf, FzfOptions, FzfResultItem } from 'fzf';
|
||||
|
||||
import PopupWindow, { PopupWindow as PopupWindowClass } from '../misc/popup-window';
|
||||
import { centerCursor } from '../../lib';
|
||||
|
||||
export interface SortedListProps<T> {
|
||||
create_list: () => T[]
|
||||
create_row: (item: T) => Gtk.Widget
|
||||
fzf_options?: FzfOptions<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: PopupWindowClass;
|
||||
|
||||
readonly create_list: () => T[];
|
||||
readonly create_row: (item: T) => Gtk.Widget;
|
||||
readonly fzf_options: FzfOptions<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,
|
||||
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 should be okay
|
||||
this.fzf_results = (new Fzf(this.item_list, this.fzf_options)).find(text);
|
||||
list.invalidate_sort();
|
||||
|
||||
const visibleApplications = list.get_children().filter((row) => row.visible).length;
|
||||
|
||||
placeholder.reveal_child = visibleApplications <= 0;
|
||||
};
|
||||
|
||||
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(() => {
|
||||
(list.get_children() as Gtk.ListBoxRow[])
|
||||
.forEach((child) => {
|
||||
child.destroy();
|
||||
});
|
||||
|
||||
this.item_list = this.create_list();
|
||||
|
||||
this.item_list
|
||||
.flatMap((prop) => this.create_row(prop))
|
||||
.forEach((child) => {
|
||||
list.add(child);
|
||||
});
|
||||
|
||||
list.show_all();
|
||||
on_text_change('');
|
||||
});
|
||||
|
||||
this.window = (
|
||||
<PopupWindow
|
||||
name={name}
|
||||
keymode={Astal.Keymode.ON_DEMAND}
|
||||
on_open={() => {
|
||||
entry.text = '';
|
||||
centerCursor();
|
||||
}}
|
||||
>
|
||||
<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: 600px;"
|
||||
hscroll={Gtk.PolicyType.NEVER}
|
||||
vscroll={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<box vertical>
|
||||
{list}
|
||||
{placeholder}
|
||||
</box>
|
||||
</scrollable>
|
||||
</eventbox>
|
||||
</box>
|
||||
</PopupWindow>
|
||||
) as PopupWindowClass;
|
||||
|
||||
this.create_list = create_list;
|
||||
this.create_row = create_row;
|
||||
this.fzf_options = fzf_options;
|
||||
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;
|
||||
}
|
27
nixosModules/ags/v2/widgets/misc/style.scss
Normal file
27
nixosModules/ags/v2/widgets/misc/style.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
.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 $accent_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue