diff options
Diffstat (limited to '.config/awesome/ui/fresnel/init.lua')
| -rw-r--r-- | .config/awesome/ui/fresnel/init.lua | 472 |
1 files changed, 297 insertions, 175 deletions
diff --git a/.config/awesome/ui/fresnel/init.lua b/.config/awesome/ui/fresnel/init.lua index 5d091b6..dc8e333 100644 --- a/.config/awesome/ui/fresnel/init.lua +++ b/.config/awesome/ui/fresnel/init.lua @@ -1,5 +1,6 @@ -local al_prompt = require "lib.bling.widget.app_launcher.prompt" +local text_input = require "ui.fresnel.text_input" local awful = require "awful" +local qdebug = require "quarrel.debug" local cfg = require "misc.cfg" local gshape = require "gears.shape" local gtable = require "gears.table" @@ -13,19 +14,142 @@ local rubato = require "lib.rubato" local wibox = require "wibox" local btn = awful.button.names -local max_entries = 10 +local MAX_VISIBLE_ENTRIES = 10 +local DEFAULT_SCROLL_AMOUNT = 0 +local DEFAULT_SELECTED_IDX = 1 -local fresnel = {} +---@class (exact) Fresnel +---@field private _toggled boolean Whether fresnel is open +---@field private _entries (Entry|string)[] +---@field private _scroll_amount integer How many entries down are we +---@field private _prev_scroll_amount integer The _scroll_amount of the previous cycle +---@field private _visible_entries Entry[] The currently visible entries +---@field private _selected_idx integer The index of the currently selected entry. Is in the range 1.. +---@field private _prev_selected_idx integer The _selected_idx of the previous cycle +---@field private _active_lenses integer The number of lense titles in the _entries table +---@field private _lense_header_positions integer[] The positions of all the lense titles +---@field private _prev_text string +---@field private _first_query boolean +---@field private _make_header fun(string): wibox.container.background +---@field private _w_popup wibox.widget.base +---@field private _w_prompt wibox.widget.base +---@field private _w_status wibox.widget.base +---@field private _l_entries wibox.widget.base +---@field private _t_opacity table +---@field private _t_height table +local fresnel = { + _toggled = false, + _entries = {}, + _scroll_amount = DEFAULT_SCROLL_AMOUNT, + _prev_scroll_amount = DEFAULT_SCROLL_AMOUNT, + _visible_entries = {}, + _selected_idx = DEFAULT_SELECTED_IDX, + _prev_selected_idx = DEFAULT_SELECTED_IDX, + _lense_header_positions = {} +} -fresnel._toggled = false -fresnel._entries_exec = {} -fresnel._entries_offset = 0 -fresnel._entries_count = 0 -fresnel._visible_entries = 0 -fresnel._selected_index = 1 +---@private +---@param lense_name string +function fresnel._make_header(lense_name) + return wibox.widget { + widget = wibox.container.background, + fg = qcolor.palette.fg.low, + { + widget = wibox.container.margin, + margins = qui.PADDING, + { + { + widget = wibox.container.place, + { + widget = wibox.container.background, + bg = qcolor.palette.border(), + forced_height = qui.BORDER_WIDTH, + forced_width = qui.BIG_PADDING * 2, + }, + }, + { + widget = wibox.container.constraint, + strategy = "max", + height = qui.CHAR_HEIGHT, + { + widget = wibox.widget.textbox, + text = lense_name, + }, + }, + { + widget = wibox.container.place, + fill_horizontal = true, + content_fill_horizontal = true, + { + widget = wibox.container.background, + bg = qcolor.palette.border(), + forced_height = qui.BORDER_WIDTH, + }, + }, + layout = wibox.layout.fixed.horizontal, + spacing = qui.BIG_PADDING, + }, + }, + } +end -function fresnel:_exec_entry(entry_index) - local exec = self._entries_exec[entry_index] +---@private +---@param entry Entry +function fresnel:_make_entry(entry) + ---@class FresnelEntry : wibox.widget.base + ---@field _selected boolean + local entry_widget = wibox.widget { + widget = wibox.container.background, + shape = qui.shape, + { + widget = wibox.container.margin, + margins = qui.PADDING, + { + widget = wibox.container.constraint, + strategy = "max", + height = qui.CHAR_HEIGHT, + { + widget = wibox.widget.textbox, + text = entry.message, + }, + }, + }, + buttons = { + awful.button { + modifiers = {}, + button = btn.LEFT, + on_press = function() + fresnel:_exec_entry(entry) + end, + }, + }, + _selected = false, + } + + entry_widget:connect_signal("mouse::enter", function() + if entry_widget._selected == true then + return + end + entry_widget.bg = qcolor.palette.bg.high + end) + + entry_widget:connect_signal("mouse::leave", function() + if entry_widget._selected == true then + return + end + entry_widget.bg = qcolor.palette.bg() + end) + + return entry_widget +end + +---@private +---@param entry integer|Entry +function fresnel:_exec_entry(entry) + if type(entry) == "number" then + entry = self._visible_entries[entry] + end + local exec = entry.exec if type(exec) ~= "userdata" and type(exec) ~= "nil" then if exec[2] then awful.spawn(cfg.terminal .. " -e /bin/sh -c " .. exec[1] .. " 1>/dev/null 2>&1") @@ -35,172 +159,166 @@ function fresnel:_exec_entry(entry_index) end end -function fresnel:_update(query, scrolled) +---@private +---@param query string? +function fresnel:_update(query) query = query or "" - scrolled = scrolled or false + self._entries = {} + self._lense_header_positions = {} + self._active_lenses = 0 - if not scrolled then - self._selected_index = 1 - self._entries_offset = 0 + for _, lense in ipairs(qnative.lenses) do + local entries = lense:query(query) + if type(entries) == "table" then + table.insert(self._entries, lense.name) + table.insert(self._lense_header_positions, #self._entries) + if #entries > 0 then -- Entry[] + self._entries = gtable.join(self._entries, entries) + elseif entries.message then -- Entry + table.insert(self._entries, entries) + end + self._active_lenses = self._active_lenses + 1 + end -- either empty Entry[] or nil, in which case we shouldn't display them end - local layout = self._popup.widget:get_children_by_id("entry_layout")[1] - local status = self._popup.widget:get_children_by_id("status")[1] - local all_providers = {} - local entries_count = 0 + -- reset the scroll + if type(self._entries[1]) == "string" then + self._selected_idx = 2 -- to avoid selecting the header first + else + self._selected_idx = DEFAULT_SELECTED_IDX + end + self._scroll_amount = DEFAULT_SCROLL_AMOUNT + fresnel:_render(true) + self._first_query = false +end - self._entries_exec = {} - layout:reset() +---@private +---@param update boolean +function fresnel:_render(update) + local visible_entries_end = math.min(self._scroll_amount + MAX_VISIBLE_ENTRIES, #self._entries) + self._visible_entries = qtable.slice(self._entries, self._scroll_amount + 1, visible_entries_end) - for _, provider in qtable.opairs(qnative.lenses) do - local entries = provider(query) + local layout = fresnel._l_entries - table.sort(entries, function(a, b) - return a.message < b.message - end) + if self._scroll_amount ~= self._prev_scroll_amount or update or self._first_query then + layout:reset() + for i, entry in ipairs(self._visible_entries) do + if type(entry) == "string" then + layout:add(self._make_header(entry)) + else + local entry_widget = self:_make_entry(entry) + if self._selected_idx == i then + entry_widget._selected = true + entry_widget.bg = qcolor.palette.bg.high + end + layout:add(entry_widget) + end + end + elseif self._selected_idx ~= self._prev_selected_idx then + local entry_widget = layout.children[self._selected_idx] --[[@as FresnelEntry ]] + entry_widget._selected = true + entry_widget.bg = qcolor.palette.bg.high - all_providers = gtable.join(all_providers, entries) + local prev_entry_widget = layout.children[self._prev_selected_idx] --[[@as FresnelEntry ]] + prev_entry_widget._selected = false + prev_entry_widget.bg = qcolor.palette.bg() end - self._entries_count = #all_providers - - for i, entry in ipairs(all_providers) do - if i > self._entries_offset then - if entries_count == max_entries then - break - end - table.insert(self._entries_exec, entry.exec) - - local entry_widget = wibox.widget { - widget = wibox.container.background, - shape = qui.shape, - { - widget = wibox.container.margin, - margins = qui.PADDING, - { - widget = wibox.container.constraint, - strategy = "max", - height = qui.CHAR_HEIGHT, - { - { - widget = wibox.container.background, - fg = qcolor.palette.low, - { - widget = wibox.widget.textbox, - text = entry.provider .. " | ", - }, - }, - { - widget = wibox.widget.textbox, - text = entry.message, - }, - spacing = qui.PADDING, - layout = wibox.layout.fixed.horizontal, - }, - }, - }, - buttons = { - awful.button { - modifiers = {}, - button = btn.LEFT, - on_press = function() - self:_exec_entry(i) - end, - }, - }, - _selected = false, - } + local headers_passed = 0 + local current_position = self._scroll_amount + self._selected_idx + for _, position in ipairs(self._lense_header_positions) do + if current_position > position then + headers_passed = headers_passed + 1 + end + end + self._w_status.text = (self._scroll_amount + self._selected_idx - headers_passed) .. "/" .. (#self._entries - self._active_lenses) + self._prev_scroll_amount = self._scroll_amount + self._prev_selected_idx = self._selected_idx +end - if self._selected_index + self._entries_offset == i then - entry_widget._selected = true - entry_widget.bg = qcolor.palette.bg.high - end +---@param up boolean Whether to scroll up or down +function fresnel:scroll(up) + local direction = up and -1 or 1 + local offset = direction - entry_widget:connect_signal("mouse::enter", function() - if entry_widget._selected == true then - return - end - entry_widget.bg = qcolor.palette.bg.high - end) - entry_widget:connect_signal("mouse::leave", function() - if entry_widget._selected == true then - return - end - entry_widget.bg = qcolor.palette.bg() - end) + if type(self._entries[self._scroll_amount + self._selected_idx + offset]) == "string" then + offset = offset + direction + end - layout:add(entry_widget) + local new_selected_idx = self._selected_idx + offset + local new_position = self._scroll_amount + new_selected_idx + local new_scroll_amount = self._scroll_amount + offset - entries_count = entries_count + 1 - self._visible_entries = entries_count + if new_position < 1 then + if self._scroll_amount > 0 then + self._scroll_amount = self._scroll_amount + direction + self._selected_idx = self._selected_idx - direction + self:_render(false) end + return + elseif new_position > #self._entries then + return + end + + if up and new_selected_idx <= 0 then + self._scroll_amount = new_scroll_amount + elseif not up and new_selected_idx > #self._visible_entries then + self._scroll_amount = new_scroll_amount + else + self._selected_idx = new_selected_idx end - status.text = self._entries_offset + self._selected_index .. "/" .. self._entries_count + self:_render(false) end -fresnel._text = "" -fresnel._prompt = al_prompt { - prompt = "", - reset_on_stop = true, - ul_cursor = "low", - bg_cursor = qcolor.palette.bg.high, - changed_callback = function(text) - if fresnel._text == text then - return - end - if fresnel._toggled == false then - return - end - fresnel:_update(text) - fresnel._text = text - end, - keypressed_callback = function(mod, key) - if key == "Escape" or key == " " and mod.Mod4 then - fresnel:hide() - elseif key == "Return" then - fresnel:_exec_entry(fresnel._selected_index) - fresnel:hide() - elseif key == "Up" then - local next_index = fresnel._selected_index - 1 - if next_index < 1 and fresnel._entries_offset == 0 then - return - elseif next_index < 1 and fresnel._entries_offset > 0 then - fresnel._entries_offset = fresnel._entries_offset - 1 - else - fresnel._selected_index = next_index - end +fresnel._prev_text = "" +fresnel._w_prompt = wibox.widget { + widget = text_input, + reset_on_unfocus = true, + unfocus_keys = {}, +} - fresnel:_update(fresnel._text, true) - elseif key == "Down" then - local next_index = fresnel._selected_index + 1 - if - next_index > fresnel._visible_entries - and fresnel._entries_offset + fresnel._visible_entries == fresnel._entries_count - then - return - elseif - next_index > fresnel._visible_entries - and fresnel._entries_offset + fresnel._visible_entries < fresnel._entries_count - then - fresnel._entries_offset = fresnel._entries_offset + 1 - else - fresnel._selected_index = next_index - end +fresnel._w_prompt:connect_signal("unfocus", function () + fresnel:_hide() +end) - fresnel:_update(fresnel._text, true) - end - end, -} +fresnel._w_prompt:connect_signal("property::text", function(_, text) + fresnel:_update(text) +end) + +fresnel._w_prompt:connect_signal("key::press", function(_, _mods, key) + -- Convert index array to hash table + local mods = {} + for _, v in ipairs(_mods) do + mods[v] = true + end + + if key == "Escape" or key == " " and mods.Mod4 then + fresnel:hide() + elseif key == "Return" then + fresnel:_exec_entry(fresnel._selected_idx) + fresnel:hide() + elseif key == "k" and mods.Mod1 then + fresnel:scroll(true) + elseif key == "j" and mods.Mod1 then + fresnel:scroll(false) + end +end) local max_height = qui.BIG_PADDING * 2 + (qui.BIG_PADDING * 2 + qui.CHAR_HEIGHT) * 10 --- + qui.PADDING --- + qui.PADDING * 9 local width = awful.screen.focused().geometry.width / 2 -fresnel._popup = qui.popup { +fresnel._l_entries = wibox.widget { + spacing = qui.PADDING, + layout = wibox.layout.fixed.vertical, +} +fresnel._w_status = wibox.widget { + widget = wibox.widget.textbox, + text = "0/0", +} +fresnel._w_popup = qui.popup { -- visible = false, ontop = true, placement = false, @@ -222,7 +340,7 @@ fresnel._popup = qui.popup { { { widget = wibox.widget.textbox, - text = ">", + markup = [[<span foreground="]] .. qcolor.palette.yellow() .. [[">></span>]], }, { widget = wibox.container.margin, @@ -237,15 +355,11 @@ fresnel._popup = qui.popup { { widget = wibox.container.background, fg = qcolor.palette.fg(), - fresnel._prompt.textbox, + fresnel._w_prompt, }, }, }, - { - widget = wibox.widget.textbox, - text = "0/0", - id = "status", - }, + fresnel._w_status, layout = wibox.layout.align.horizontal, }, }, @@ -255,52 +369,60 @@ fresnel._popup = qui.popup { margins = { top = qui.PADDING, }, - { - spacing = qui.PADDING, - layout = wibox.layout.fixed.vertical, - id = "entry_layout", - }, + fresnel._l_entries, }, layout = wibox.layout.align.vertical, }, } + function fresnel:show() + self._prev_scroll_amount = DEFAULT_SCROLL_AMOUNT + self._prev_selected_idx = DEFAULT_SELECTED_IDX + self._first_query = true self._toggled = true - self._opacity_timed.target = 1 - self._height_timed:set(max_height) + self._t_opacity.target = 1 + self._t_height:set(max_height) self:_update() - self._prompt:start() + self._w_prompt:focus() end -function fresnel:hide() +---@private +function fresnel:_hide() self._toggled = false - self._opacity_timed.target = 0 - self._height_timed:set(0) - self._prompt:stop() + self._t_opacity.target = 0 + self._t_height:set(0) + for _, lense in ipairs(qnative.lenses) do + lense:interrupt() + end +end + +function fresnel:hide() + self._w_prompt:unfocus() + self:_hide() end -fresnel._height_timed = qanim:new { +fresnel._t_height = qanim:new { duration = qvars.anim_duration, pos = 0, easing = qvars.easing, subscribed = function(pos) - fresnel._popup.shape = function(cr, w) + fresnel._w_popup.shape = function(cr, w) gshape.partially_rounded_rect(cr, w, pos, false, false, true, true, qui.BORDER_RADIUS) end end, } -- TODO: optimize the search algo to be more efficient and not require making fresnel invisible -fresnel._opacity_timed = rubato.timed { +fresnel._t_opacity = rubato.timed { duration = qvars.anim_duration, pos = 0, subscribed = function(pos) - fresnel._popup.opacity = pos + fresnel._w_popup.opacity = pos if pos == 0 then - fresnel._popup.visible = false + fresnel._w_popup.visible = false else - fresnel._popup.visible = true + fresnel._w_popup.visible = true end end, } |
