From a67da99839f1a66fc6f84b9cf08577849839f950 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Sun, 27 Jul 2025 17:51:10 -0300 Subject: [PATCH] :boom: fix(runner): result selection not working, scrolledwindow not scrolling automatically with focused row --- ags/runner/Runner.tsx | 245 ++++++++++++++++++++++----------- ags/style.scss | 16 +-- ags/style/_apps-window.scss | 4 +- ags/style/_center-window.scss | 6 +- ags/style/_control-center.scss | 5 +- ags/style/_osd.scss | 20 +-- ags/style/_runner.scss | 68 ++++----- ags/widget/PopupWindow.tsx | 6 +- 8 files changed, 214 insertions(+), 156 deletions(-) diff --git a/ags/runner/Runner.tsx b/ags/runner/Runner.tsx index 62329eb..d7c3bd1 100644 --- a/ags/runner/Runner.tsx +++ b/ags/runner/Runner.tsx @@ -1,14 +1,13 @@ import { Astal, Gdk, Gtk } from "ags/gtk4"; -import { PopupWindow } from "../widget/PopupWindow"; +import { getPopupWindowContainer, PopupWindow } from "../widget/PopupWindow"; import { updateApps } from "../scripts/apps"; import { ResultWidget, ResultWidgetProps } from "./widgets/ResultWidget"; import { Windows } from "../windows"; -import { createState, For } from "ags"; import { timeout } from "ags/time"; -import GObject from "ags/gobject"; import AstalHyprland from "gi://AstalHyprland"; import AstalIO from "gi://AstalIO"; +import GObject from "gi://GObject?version=2.0"; export namespace Runner { @@ -42,17 +41,38 @@ export interface Plugin { } export let instance: (Astal.Window|null) = null; + let gtkEntry: (Gtk.SearchEntry|null) = null; const plugins = new Set(); +const ignoredKeys = [ + Gdk.KEY_space, + Gdk.KEY_Shift_L, + Gdk.KEY_Shift_R, + Gdk.KEY_Shift_Lock, + Gdk.KEY_Return, + Gdk.KEY_Tab, + Gdk.KEY_Control_L, + Gdk.KEY_Control_R, + Gdk.KEY_Alt_L, + Gdk.KEY_Alt_R, + Gdk.KEY_Option, + Gdk.KEY_Super_L, + Gdk.KEY_Super_R,, + Gdk.KEY_F5, + Gdk.KEY_Up, + Gdk.KEY_Down, + Gdk.KEY_Left, + Gdk.KEY_Right +]; + export function close() { instance?.close(); } - export function regExMatch(search: string, item: (string|number)): boolean { search = search.replace(/[\\^$.*?()[\]{}|]/g, "\\$&"); if(typeof item === "number") return new RegExp(`${search.split('').map(c => - `${c}`).join('')}`, + `[${c}]`).join('')}`, "g").test(item.toString()); return new RegExp(`${search.split('').map(c => @@ -91,6 +111,7 @@ export function openDefault(initialText?: string) { return Runner.openRunner({ entryPlaceHolder: "Start typing...", initialText, + showResultsPlaceHolderOnStartup: false, resultsLimit: 24 } as Runner.RunnerProps, [ { @@ -138,52 +159,93 @@ export function openDefault(initialText?: string) { ]); } -export function openRunner(props: RunnerProps, placeholder?: Array): Astal.Window { +function getPluginResults(input: string, limit?: number): Array { + if(limit != null) + limit = Math.abs(Math.floor(limit)); + + let calledPlugins: Array = getPlugins().filter((plugin) => + plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true + ).sort((plugin) => plugin.prefix != null ? 0 : 1); + + for(const plugin of calledPlugins) { + if(plugin.prioritize) { + calledPlugins = [ plugin ]; + break; + } + } + + const results = calledPlugins.map(plugin => plugin.handle( + plugin.prefix ? input.replace(plugin.prefix, "") : input) + ).filter(value => value !== undefined && value !== null).flat(1); + + return limit != null && limit !== Infinity ? + results.splice(0, limit) + : results; +} + +function updateResultsList(listbox: Gtk.ListBox, input: string, placeholders?: Array) { + const newResults: Array = [], + scrolledWindow = listbox.parent.parent as Gtk.ScrolledWindow; + + listbox.remove_all(); + getPluginResults(input).forEach((result) => { + listbox.insert( as ResultWidget, -1); + newResults.push(result); + }); + + // Insert placeholder if there are no results + if(placeholders && newResults.length < 1) + placeholders.forEach(phdlr => listbox.insert( + as ResultWidget, -1 + )); + + newResults.length > 0 ? + (!scrolledWindow.visible && scrolledWindow.show()) + : scrolledWindow.hide(); +} + +function selectPreviousItem(listbox: Gtk.ListBox) { + const selectedRow = listbox.get_selected_row(); + const prevRow = selectedRow?.get_prev_sibling(); + + if(!prevRow || selectedRow === listbox.get_row_at_index(0)) + return; + + const viewport = listbox.parent as Gtk.Viewport; + const vadjustment = (viewport.parent as Gtk.ScrolledWindow).get_vadjustment(); + const [, , prevRowY] = prevRow.translate_coordinates(viewport, + prevRow.get_allocation().x, prevRow.get_allocation().y); + + listbox.select_row(prevRow as Gtk.ListBoxRow); + if(prevRowY < vadjustment.get_value()) + vadjustment.set_value(prevRowY); +} + +function selectNextItem(listbox: Gtk.ListBox) { + const selectedRow = listbox.get_selected_row(); + const nextRow = selectedRow?.get_next_sibling(); + + if(!nextRow || selectedRow === listbox.get_last_child()) + return; + + const viewport = listbox.parent as Gtk.Viewport; + const vadjustment = (viewport.parent as Gtk.ScrolledWindow).get_vadjustment(); + const nextRowVAllocation = (nextRow.get_allocation().y + nextRow.get_allocation().height); + + listbox.select_row(nextRow as Gtk.ListBoxRow); + if(nextRowVAllocation > viewport.get_allocation().height) + vadjustment.set_value(nextRow.get_allocation().y - viewport.get_allocation().height + nextRow.get_allocation().height); +} + +export function openRunner(props: RunnerProps, placeholders?: Array): Astal.Window { props.width ??= 780; props.height ??= 420; - const connections: Map = new Map(); - const [results, setResults] = createState([] as Array); - let clickTimeout: AstalIO.Time|undefined; - - function getPluginResults(input: string): Array { - let calledPlugins: Array = getPlugins().filter((plugin) => - plugin.prefix ? (input.startsWith(plugin.prefix) ? true : false) : true - ).sort((plugin) => plugin.prefix != null ? 0 : 1); - - for(const plugin of calledPlugins) { - if(plugin.prioritize) { - calledPlugins = [ plugin ]; - break; - } - } - - const results = calledPlugins.map(plugin => plugin.handle( - plugin.prefix ? input.replace(plugin.prefix, "") : input) - ).filter(value => value !== undefined && value !== null).flat(1); - - return props?.resultsLimit != null && - props.resultsLimit !== Infinity ? - results.splice(0, props.resultsLimit) - : results; - } - - function updateResultsList(input: string) { - const newResults: Array = []; - - // Insert placeholder if there are no results - if(placeholder && results.get().length === 0) - newResults.push(...placeholder); - - getPluginResults(input).forEach((result) => { - newResults.unshift(result); - }); - - setResults(newResults); - } + let clickTimeout: AstalIO.Time|undefined, + results: Array = []; if(!instance) - instance = Windows.getDefault().createWindowForFocusedMonitor((mon: number) => + instance = Windows.getDefault().createWindowForFocusedMonitor((mon, root) => ): Ast props.initialText && Runner.setEntryText(props.initialText); - }} actionKeyPressed={(_, keyval) => { - if(!gtkEntry!.has_focus && keyval !== Gdk.KEY_F5 - && keyval !== Gdk.KEY_Down && keyval !== Gdk.KEY_Up - && keyval !== Gdk.KEY_Return) { - gtkEntry!.grab_focus(); - return; + }} actionKeyPressed={(self, keyval) => { + const listbox = ((getPopupWindowContainer(self).get_first_child()! + .get_last_child()! as Gtk.ScrolledWindow).get_child()! as Gtk.Viewport) + .get_child()! as Gtk.ListBox; + + switch(keyval) { + case Gdk.KEY_F5: + updateApps(); + return; + + case Gdk.KEY_Left: + case Gdk.KEY_Up: + selectPreviousItem(listbox); + return; + + case Gdk.KEY_Right: + case Gdk.KEY_Down: + selectNextItem(listbox); + return; } - keyval === Gdk.KEY_F5 && - updateApps(); - }} onCloseRequest={() => { - connections.forEach((id, obj) => GObject.signal_handler_is_connected(obj, id) && - obj.disconnect(id)); - - gtkEntry = null; + for(const key of ignoredKeys) { + if(keyval === key) + return; + } + !gtkEntry?.hasFocus && + gtkEntry?.grab_focus(); + }} actionClosed={() => { [...plugins.values()].forEach(plugin => plugin?.onClose?.()); + root.dispose(); instance = null; + gtkEntry = null; }}> - + gtkEntry = self} - onSearchChanged={(self) => { - updateResultsList(self.text); - const listbox = self.parent.get_last_child()?.get_first_child()?.get_first_child() as Gtk.ListBox; + $={(self) => { + gtkEntry = self; + const controllerKey = Gtk.EventControllerKey.new(), + conns = new Map; + self.add_controller(controllerKey); + + conns.set(controllerKey, controllerKey.connect("key-released", (_, keyval) => { + if(keyval === Gdk.KEY_Escape) + (self.parent.parent.parent.parent as Astal.Window)?.close(); + + return true; + })); + + conns.set(self, self.connect("destroy", () => conns.forEach((id, obj) => + obj.disconnect(id)))); + }} searchDelay={0} onSearchChanged={(self) => { + const listbox = self.get_next_sibling()?.get_first_child()?.get_first_child() as Gtk.ListBox; + updateResultsList(listbox, self.text, placeholders); listbox.get_row_at_index(0) && listbox.select_row(listbox.get_row_at_index(0)); @@ -231,25 +321,26 @@ export function openRunner(props: RunnerProps, placeholder?: Array): Ast if(resultWidget instanceof ResultWidget) { resultWidget.actionClick(); - resultWidget.closeOnClick && Runner.close(); + resultWidget.closeOnClick && + Runner.close(); } }} /> - { - if(row instanceof ResultWidget && !clickTimeout) { + { + const child = row.get_child()!; + + if(child instanceof ResultWidget && !clickTimeout) { clickTimeout = timeout(250, () => clickTimeout = undefined); - row.actionClick?.(); + child.actionClick?.(); + child.closeOnClick && + Runner.close(); } - }}> - - {(res: Result) => } - - + }} /> as Astal.Window diff --git a/ags/style.scss b/ags/style.scss index 6ddf3ea..560c54a 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -17,10 +17,6 @@ * { @include mixins.reset-props; - - /*&:focus { - box-shadow: inset 0 0 0 2px colors.$fg-primary; - }*/ } entry { @@ -55,6 +51,7 @@ entry { & .options { & button { + @include mixins.button-reactive-primary; background: colors.$bg-primary; border-radius: 12px; padding: 9px 6px; @@ -68,14 +65,6 @@ entry { left: 4px; right: 4px; }; - - &:hover { - background: colors.$bg-secondary; - } - - &:focus { - box-shadow: inset 0 0 0 2px colors.$fg-primary; - } } } @@ -275,15 +264,12 @@ trough { trough highlight { background: wal.$color1; min-height: .9em; - border-top-left-radius: inherit; - border-bottom-left-radius: inherit; } trough slider { border-radius: 50%; margin: -2px 0; background: wal.$foreground; - margin-left: -1px; min-width: 1.2em; min-height: 1.2em; } diff --git a/ags/style/_apps-window.scss b/ags/style/_apps-window.scss index b737b06..3a67c37 100644 --- a/ags/style/_apps-window.scss +++ b/ags/style/_apps-window.scss @@ -25,7 +25,7 @@ & > flowboxchild { & > button { - padding: 8px; + padding: 10px; border-radius: 24px; & image { @@ -33,7 +33,7 @@ } & label { - margin-top: 6px; + margin-top: 24px; text-shadow: 1px 1px 1px rgba(colors.$bg-primary, .2); font-weight: 500; } diff --git a/ags/style/_center-window.scss b/ags/style/_center-window.scss index 5c019c7..c443c3b 100644 --- a/ags/style/_center-window.scss +++ b/ags/style/_center-window.scss @@ -81,12 +81,12 @@ & trough { border-radius: 4px; - min-height: .6em; } & trough highlight { - border-radius: 4px; - min-height: .6em; + min-height: .65em; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; } & .bottom { diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 78970fe..324eef9 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -17,14 +17,15 @@ &:first-child { margin-top: 0; } + &:last-child { margin-bottom: 0; } } - /*& eventbox:focus, & button:focus { + & button:focus-visible { box-shadow: inset 0 0 0 1px colors.$fg-primary; - }*/ + } & .quickactions { margin-bottom: .8em; diff --git a/ags/style/_osd.scss b/ags/style/_osd.scss index 3ae0f81..57ea437 100644 --- a/ags/style/_osd.scss +++ b/ags/style/_osd.scss @@ -16,24 +16,18 @@ margin-top: -6px; .device { - margin-bottom: 5px; + margin-bottom: 6px; font-size: 14px; font-weight: 600; } - levelbar { - trough block { - border-radius: 3px; - background: colors.$bg-primary; + levelbar trough block { + border-radius: 3px; + background: colors.$bg-primary; - &.empty { - border-radius: 3px; - } - - &.filled { - min-height: 8px; - background: colors.$bg-secondary; - } + &.filled { + min-height: 8px; + background: colors.$bg-secondary; } } diff --git a/ags/style/_runner.scss b/ags/style/_runner.scss index 1860085..d7433f3 100644 --- a/ags/style/_runner.scss +++ b/ags/style/_runner.scss @@ -28,11 +28,32 @@ & list { padding: 6px; - padding-top: 0; - & > *:selected > .result, - & > *:active > .result, - & > *:hover > .result { + & .result { + padding: 10px; + background: colors.$bg-primary; + margin: 2px 0; + border-radius: 14px; + + & image { + -gtk-icon-size: 28px; + margin-right: 6px; + } + + & .title { + font-weight: 500; + font-size: 16px; + } + + & .description { + font-size: 12px; + color: colors.$fg-disabled; + } + } + + & > *:selected .result, + & > *:active .result, + & > *:hover .result { background: colors.$bg-secondary; } @@ -44,43 +65,4 @@ margin-bottom: 0; } } - - & trough { - margin-bottom: 10px; - } - - & list .result { - padding: 10px; - background: colors.$bg-primary; - margin: 2px 0; - border-radius: 14px; - - & icon { - font-size: 28px; - margin-right: 6px; - } - - & .title { - font-weight: 500; - font-size: 16px; - } - - & .description { - font-size: 12px; - color: colors.$fg-disabled; - } - } - - & .not-found { - padding-top: 24px; - - & icon { - font-size: 64px; - margin-bottom: .4em; - } - - & label { - font-size: 16px; - } - } } diff --git a/ags/widget/PopupWindow.tsx b/ags/widget/PopupWindow.tsx index 61a0435..8d3dff3 100644 --- a/ags/widget/PopupWindow.tsx +++ b/ags/widget/PopupWindow.tsx @@ -109,7 +109,7 @@ export function PopupWindow(props: PopupWindowProps): GObject.Object { } })); - conns.set(keyController, keyController.connect("key-released", (_, keyval, keycode) => { + conns.set(keyController, keyController.connect("key-pressed", (_, keyval, keycode) => { if(keyval === Gdk.KEY_Escape) { conns.forEach((id, obj) => { obj.disconnect(id); @@ -158,3 +158,7 @@ export function PopupWindow(props: PopupWindowProps): GObject.Object { as Astal.Window; } + +export function getPopupWindowContainer(popupWindow: Astal.Window): Gtk.Box { + return popupWindow.get_child()!.get_first_child() as Gtk.Box; +}