diff --git a/src/components/menus/apps/index.tsx b/src/components/menus/apps/index.tsx new file mode 100644 index 000000000..729a759ba --- /dev/null +++ b/src/components/menus/apps/index.tsx @@ -0,0 +1,169 @@ +import DropdownMenu from '../shared/dropdown/index.js'; +import options from 'src/options.js'; +import Variable from 'astal/variable'; +import { bind } from 'astal/binding'; +import { RevealerTransitionMap } from 'src/lib/constants/options.js'; +import { App, Gtk } from 'astal/gtk3'; +import Separator from 'src/components/shared/Separator.js'; +import AstalApps from 'gi://AstalApps?version=0.1' +import { icon, launchApp } from 'src/lib/utils.js'; +import { Entry, EntryProps, Scrollable } from 'astal/gtk3/widget'; + +import PopupWindow from '../shared/popup/index.js'; + +const apps = new AstalApps.Apps({ + nameMultiplier: 2, + keywordsMultiplier: 2, + executableMultiplier: 1, + entryMultiplier: 0.5, + categoriesMultiplier: 0.5, +}); + +interface ApplicationItemProps { + app: AstalApps.Application; + onLaunched?: () => void; +} + +const ApplicationItem = ({ app, onLaunched }: ApplicationItemProps): JSX.Element => { + return ( + + ); +} + + +function useRef() { + let ref: T | null = null; + + return { + set: (r: T) => { ref = r }, + get: () => ref + } +} + +function useApplicationsFilter() { + const filter = Variable('') + + const list = bind(filter).as((f) => { + // show all apps by default + if (!f) return apps.get_list() + // if the filter is a single character, show all apps that start with that character + if (f.length === 1) return apps.get_list().filter((app) => app.name.toLowerCase().startsWith(f.toLowerCase())) + // otherwise, do a fuzzy search (this method wont filter with a single character) + return apps.fuzzy_query(f) + }) + + return { filter, list } +} + +interface ApplicationLauncherProps { + visible: Variable; + onLaunched?: () => void; +} + +const SearchBar = ({ value, setup, onActivate }: { value?: Variable; setup?: (self: Entry) => void; onActivate?: EntryProps['onActivate'] }) => { + return ( + + + value.set(entry.text))} /> + + + + + + ) +} + + +const ApplicationLauncher = ({ visible, onLaunched }: ApplicationLauncherProps): JSX.Element => { + const entry = useRef() + const scrollable = useRef() + + const { filter, list } = useApplicationsFilter() + + const onFilterReturn = () => { + const first = list.get()[0] + if (!first) return; + launchApp(first) + onLaunched?.() + } + + // focus the entry when the menu is shown + const onShow = () => { + entry.get()?.grab_focus() + } + visible.subscribe(v => v && onShow()); + + const onHide = () => { + // clear the filter when the menu is hidden + filter.set('') + // TODO: reset scroll position + } + visible.subscribe(v => !v && onHide); + + return ( + + + + + + {list.as(apps => apps.map((app) => ))} + + + + + ) +} + +/** + * track the visibility of a window + * this is necessary because menu are realized at startup and never destroyed + * making onRealize and onDestroy unreliable for lifecycle management + */ +function useWindowVisibility(windowName: string) { + const visible = Variable(!!App.get_window(windowName)?.visible); + + App.connect('window-toggled', (_, window) => { + if (window.name !== windowName) return; + visible.set(window.visible); + }) + + return visible; +} + +export const ApplicationsDropdownMenu = (): JSX.Element => { + const visible = useWindowVisibility('applicationsdropdownmenu'); + + const close = () => App.get_window('applicationsdropdownmenu')?.set_visible(false); + + return ( + RevealerTransitionMap[transition])} + > + + + ); +}; + + +export const ApplicationsMenu = (): JSX.Element => { + const visible = useWindowVisibility('applicationsmenu'); + + const close = () => App.get_window('applicationsmenu')?.set_visible(false); + + return ( + RevealerTransitionMap[transition])}> + + + ) +} \ No newline at end of file diff --git a/src/components/menus/exports.ts b/src/components/menus/exports.ts index 913a42f7b..0b3bb149d 100644 --- a/src/components/menus/exports.ts +++ b/src/components/menus/exports.ts @@ -9,6 +9,7 @@ import CalendarMenu from './calendar/index.js'; import EnergyMenu from './energy/index.js'; import DashboardMenu from './dashboard/index.js'; import PowerDropdown from './powerDropdown/index.js'; +import {ApplicationsDropdownMenu, ApplicationsMenu} from './apps/index'; export const DropdownMenus = [ AudioMenu, @@ -20,6 +21,7 @@ export const DropdownMenus = [ EnergyMenu, DashboardMenu, PowerDropdown, + ApplicationsDropdownMenu ]; -export const StandardWindows = [PowerMenu, Verification]; +export const StandardWindows = [PowerMenu, Verification, ApplicationsMenu];