From ccdde8135603ba2d96b896a96975d52f94b75350 Mon Sep 17 00:00:00 2001 From: matt1432 <matt@nelim.org> Date: Mon, 28 Oct 2024 18:18:07 -0400 Subject: [PATCH] refactor(agsV2): abstract sorted-list code --- nixosModules/ags/v2/style.scss | 1 + .../ags/v2/widgets/applauncher/main.tsx | 139 +++----------- .../ags/v2/widgets/applauncher/style.scss | 32 +--- .../ags/v2/widgets/icon-browser/main.tsx | 134 ++------------ .../ags/v2/widgets/icon-browser/style.scss | 34 +--- .../ags/v2/widgets/misc/popup-window.tsx | 4 +- .../ags/v2/widgets/misc/sorted-list.tsx | 174 ++++++++++++++++++ nixosModules/ags/v2/widgets/misc/style.scss | 27 +++ 8 files changed, 250 insertions(+), 295 deletions(-) create mode 100644 nixosModules/ags/v2/widgets/misc/sorted-list.tsx create mode 100644 nixosModules/ags/v2/widgets/misc/style.scss diff --git a/nixosModules/ags/v2/style.scss b/nixosModules/ags/v2/style.scss index fbbbb6e0..7717419f 100644 --- a/nixosModules/ags/v2/style.scss +++ b/nixosModules/ags/v2/style.scss @@ -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'; diff --git a/nixosModules/ags/v2/widgets/applauncher/main.tsx b/nixosModules/ags/v2/widgets/applauncher/main.tsx index 89e7c812..5a6bab62 100644 --- a/nixosModules/ags/v2/widgets/applauncher/main.tsx +++ b/nixosModules/ags/v2/widgets/applauncher/main.tsx @@ -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) => { + create_row: (app) => AppItemWidget({ app }), + + fzf_options: { + selector: (app) => app.name + app.executable, + + tiebreakers: [ + (a, b) => b.item.frequency - a.item.frequency, + ], + }, + + on_row_activated: (row) => { const app = (row.get_children()[0] as AppItem).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, { - selector: (app) => app.name + app.executable, - - tiebreakers: [ - (a, b) => b.item.frequency - a.item.frequency, - ], - }); - - fzfResults = fzf.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) => { + 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> - ); -}; + }, +}); diff --git a/nixosModules/ags/v2/widgets/applauncher/style.scss b/nixosModules/ags/v2/widgets/applauncher/style.scss index 2ee89180..cbba15fc 100644 --- a/nixosModules/ags/v2/widgets/applauncher/style.scss +++ b/nixosModules/ags/v2/widgets/applauncher/style.scss @@ -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; - } + .app { + margin: 20px; + font-size: 16px; } } diff --git a/nixosModules/ags/v2/widgets/icon-browser/main.tsx b/nixosModules/ags/v2/widgets/icon-browser/main.tsx index 81dcf1c1..4ce12117 100644 --- a/nixosModules/ags/v2/widgets/icon-browser/main.tsx +++ b/nixosModules/ags/v2/widgets/icon-browser/main.tsx @@ -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> - ); -}; + }, +}); diff --git a/nixosModules/ags/v2/widgets/icon-browser/style.scss b/nixosModules/ags/v2/widgets/icon-browser/style.scss index 167997c7..c5669131 100644 --- a/nixosModules/ags/v2/widgets/icon-browser/style.scss +++ b/nixosModules/ags/v2/widgets/icon-browser/style.scss @@ -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 { - margin: 20px; - font-size: 16px; - } - } - - .placeholder { - font-size: 20px; - } - } +.icon-browser .icon-list row box { + margin: 20px; + font-size: 16px; } diff --git a/nixosModules/ags/v2/widgets/misc/popup-window.tsx b/nixosModules/ags/v2/widgets/misc/popup-window.tsx index dd1b4f7e..afceb7f4 100644 --- a/nixosModules/ags/v2/widgets/misc/popup-window.tsx +++ b/nixosModules/ags/v2/widgets/misc/popup-window.tsx @@ -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 diff --git a/nixosModules/ags/v2/widgets/misc/sorted-list.tsx b/nixosModules/ags/v2/widgets/misc/sorted-list.tsx new file mode 100644 index 00000000..d46397fa --- /dev/null +++ b/nixosModules/ags/v2/widgets/misc/sorted-list.tsx @@ -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; +} diff --git a/nixosModules/ags/v2/widgets/misc/style.scss b/nixosModules/ags/v2/widgets/misc/style.scss new file mode 100644 index 00000000..89fabde4 --- /dev/null +++ b/nixosModules/ags/v2/widgets/misc/style.scss @@ -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; + } + } +}