diff options
Diffstat (limited to '.config/mpv/scripts/uosc_shared/elements/Menu.lua')
-rw-r--r-- | .config/mpv/scripts/uosc_shared/elements/Menu.lua | 783 |
1 files changed, 0 insertions, 783 deletions
diff --git a/.config/mpv/scripts/uosc_shared/elements/Menu.lua b/.config/mpv/scripts/uosc_shared/elements/Menu.lua deleted file mode 100644 index aad5016..0000000 --- a/.config/mpv/scripts/uosc_shared/elements/Menu.lua +++ /dev/null @@ -1,783 +0,0 @@ -local Element = require('uosc_shared/elements/Element') - --- Menu data structure accepted by `Menu:open(menu)`. ----@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;} ----@alias MenuDataItem MenuDataValue|MenuData ----@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;} ----@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(); on_close?: fun(); on_back?: fun()} - --- Internal data structure created from `Menu`. ----@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling} ----@alias MenuStackItem MenuStackValue|MenuStack ----@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number} ----@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean} - ----@class Menu : Element -local Menu = class(Element) - ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions -function Menu:open(data, callback, opts) - local open_menu = self:is_open() - if open_menu then - open_menu.is_being_replaced = true - open_menu:close(true) - end - return Menu:new(data, callback, opts) -end - ----@param menu_type? string ----@return Menu|nil -function Menu:is_open(menu_type) - return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil -end - ----@param immediate? boolean Close immediately without fadeout animation. ----@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed. ----@overload fun(callback: fun()) -function Menu:close(immediate, callback) - if type(immediate) ~= 'boolean' then callback = immediate end - - local menu = self == Menu and Elements.menu or self - - if menu and not menu.destroyed then - if menu.is_closing then - menu:tween_stop() - return - end - - local function close() - Elements:remove('menu') - menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {} - menu:disable_key_bindings() - Elements:update_proximities() - if callback then callback() end - request_render() - end - - menu.is_closing = true - - if immediate then close() - else menu:fadeout(close) end - end -end - ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions ----@return Menu -function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end ----@param data MenuData ----@param callback fun(value: any) ----@param opts? MenuOptions -function Menu:init(data, callback, opts) - Element.init(self, 'menu', {ignores_menu = true}) - - -----@type fun() - self.callback = callback - self.opts = opts or {} - self.offset_x = 0 -- Used for submenu transition animation. - self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items - self.item_height = nil - self.item_spacing = 1 - self.item_padding = nil - self.font_size = nil - self.font_size_hint = nil - self.scroll_step = nil -- Item height + item spacing. - self.scroll_height = nil -- Items + spacings - container height. - self.opacity = 0 -- Used to fade in/out. - self.type = data.type - ---@type MenuStack Root MenuStack. - self.root = nil - ---@type MenuStack Current MenuStack. - self.current = nil - ---@type MenuStack[] All menus in a flat array. - self.all = nil - ---@type table<string, MenuStack> Map of submenus by their ids, such as `'Tools > Aspect ratio'`. - self.by_id = {} - self.key_bindings = {} - self.is_being_replaced = false - self.is_closing, self.is_closed = false, false - ---@type {y: integer, time: number}[] - self.drag_data = nil - self.is_dragging = false - - self:update(data) - - if self.mouse_nav then - if self.current then self.current.selected_index = nil end - else - for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end - end - - self:tween_property('opacity', 0, 1) - self:enable_key_bindings() - Elements.curtain:register('menu') - if self.opts.on_open then self.opts.on_open() end -end - -function Menu:destroy() - Element.destroy(self) - self:disable_key_bindings() - self.is_closed = true - if not self.is_being_replaced then Elements.curtain:unregister('menu') end - if self.opts.on_close then self.opts.on_close() end -end - ----@param data MenuData -function Menu:update(data) - self.type = data.type - - local new_root = {is_root = true} - local new_all = {} - local new_by_id = {} - local menus_to_serialize = {{new_root, data}} - local old_current_id = self.current and self.current.id - - table_assign(new_root, data, {'title', 'hint', 'keep_open'}) - - local i = 0 - while i < #menus_to_serialize do - i = i + 1 - local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2] - local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id - if not menu.is_root then - menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i) - end - menu.icon = 'chevron_right' - - -- Update items - local first_active_index = nil - menu.items = {} - - for i, item_data in ipairs(menu_data.items or {}) do - if item_data.active and not first_active_index then first_active_index = i end - - local item = {} - table_assign(item, item_data, { - 'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator', - }) - if item.keep_open == nil then item.keep_open = menu.keep_open end - - -- Submenu - if item_data.items then - item.parent_menu = menu - menus_to_serialize[#menus_to_serialize + 1] = {item, item_data} - end - - menu.items[i] = item - end - - if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end - - -- Retain old state - local old_menu = self.by_id[menu.is_root and '__root__' or menu.id] - if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end - - new_all[#new_all + 1] = menu - new_by_id[menu.is_root and '__root__' or menu.id] = menu - end - - self.root, self.all, self.by_id = new_root, new_all, new_by_id - self.current = self.by_id[old_current_id] or self.root - - self:update_content_dimensions() - self:reset_navigation() -end - ----@param items MenuDataItem[] -function Menu:update_items(items) - local data = table_shallow_copy(self.root) - data.items = items - self:update(data) -end - -function Menu:update_content_dimensions() - self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height - self.font_size = round(self.item_height * 0.48 * options.font_scale) - self.font_size_hint = self.font_size - 1 - self.item_padding = round((self.item_height - self.font_size) * 0.6) - self.scroll_step = self.item_height + self.item_spacing - - local title_opts = {size = self.font_size, italic = false, bold = false} - local hint_opts = {size = self.font_size_hint} - - for _, menu in ipairs(self.all) do - -- Estimate width of a widest item - local max_width = 0 - for _, item in ipairs(menu.items) do - local icon_width = item.icon and self.font_size or 0 - item.title_width = text_width(item.title, title_opts) - item.hint_width = text_width(item.hint, hint_opts) - local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0) - + (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0) - local estimated_width = item.title_width + item.hint_width + icon_width - + (self.item_padding * spacings_in_item) - if estimated_width > max_width then max_width = estimated_width end - end - - -- Also check menu title - title_opts.bold, title_opts.italic = true, false - local menu_title_width = text_width(menu.title, title_opts) - if menu_title_width > max_width then max_width = menu_title_width end - - menu.max_width = max_width - end - - self:update_dimensions() -end - -function Menu:update_dimensions() - -- Coordinates and sizes are of the scrollable area to make - -- consuming values in rendering and collisions easier. Title drawn above this, so - -- we need to account for that in max_height and ay position. - local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width - - for _, menu in ipairs(self.all) do - menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9)) - local title_height = (menu.is_root and menu.title) and self.scroll_step or 0 - local max_height = round((display.height - title_height) * 0.9) - local content_height = self.scroll_step * #menu.items - menu.height = math.min(content_height - self.item_spacing, max_height) - menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5)) - menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0) - menu.scroll_y = menu.scroll_y or 0 - self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits - end - - local ax = round((display.width - self.current.width) / 2) + self.offset_x - self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height) -end - -function Menu:reset_navigation() - local menu = self.current - - -- Reset indexes and scroll - self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits - if self.mouse_nav then - self:select_item_below_cursor() - else - self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil) - end - - -- Walk up the parent menu chain and activate items that lead to current menu - local parent = menu.parent_menu - while parent do - parent.selected_index = itable_index_of(parent.items, menu) - menu, parent = parent, parent.parent_menu - end - - request_render() -end - -function Menu:set_offset_x(offset) - local delta = offset - self.offset_x - self.offset_x = offset - self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by) -end - -function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end - -function Menu:get_item_index_below_cursor() - local menu = self.current - if #menu.items < 1 or self.proximity_raw > 0 then return nil end - return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items)) -end - -function Menu:get_first_active_index(menu) - menu = menu or self.current - for index, item in ipairs(self.current.items) do - if item.active then return index end - end -end - ----@param pos? number ----@param menu? MenuStack -function Menu:set_scroll_to(pos, menu) - menu = menu or self.current - menu.scroll_y = clamp(0, pos or 0, menu.scroll_height) - request_render() -end - ----@param delta? number ----@param menu? MenuStack -function Menu:set_scroll_by(delta, menu) - menu = menu or self.current - self:set_scroll_to(menu.scroll_y + delta, menu) -end - ----@param pos? number ----@param menu? MenuStack ----@param fling_options? table -function Menu:scroll_to(pos, menu, fling_options) - menu = menu or self.current - menu.fling = { - y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y), - time = mp.get_time(), duration = 0.1, easing = ease_out_sext, - } - if fling_options then table_assign(menu.fling, fling_options) end - request_render() -end - ----@param delta? number ----@param menu? MenuStack ----@param fling_options? Fling -function Menu:scroll_by(delta, menu, fling_options) - menu = menu or self.current - self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options) -end - ----@param index? integer ----@param menu? MenuStack ----@param immediate? boolean -function Menu:scroll_to_index(index, menu, immediate) - menu = menu or self.current - if (index and index >= 1 and index <= #menu.items) then - local position = round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2)) - if immediate then self:set_scroll_to(position, menu) - else self:scroll_to(position, menu) end - end -end - ----@param index? integer ----@param menu? MenuStack -function Menu:select_index(index, menu) - menu = menu or self.current - menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil - request_render() -end - ----@param value? any ----@param menu? MenuStack -function Menu:select_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:select_index(index, 5) -end - ----@param menu? MenuStack -function Menu:deactivate_items(menu) - menu = menu or self.current - for _, item in ipairs(menu.items) do item.active = false end - request_render() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:activate_index(index, menu) - menu = menu or self.current - if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end - request_render() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:activate_one_index(index, menu) - self:deactivate_items(menu) - self:activate_index(index, menu) -end - ----@param value? any ----@param menu? MenuStack -function Menu:activate_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:activate_index(index, menu) -end - ----@param value? any ----@param menu? MenuStack -function Menu:activate_one_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:activate_one_index(index, menu) -end - ----@param id string -function Menu:activate_submenu(id) - local submenu = self.by_id[id] - if submenu then - self.current = submenu - request_render() - else - msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) - end - self:reset_navigation() -end - ----@param index? integer ----@param menu? MenuStack -function Menu:delete_index(index, menu) - menu = menu or self.current - if (index and index >= 1 and index <= #menu.items) then - table.remove(menu.items, index) - self:update_content_dimensions() - self:scroll_to_index(menu.selected_index, menu) - end -end - ----@param value? any ----@param menu? MenuStack -function Menu:delete_value(value, menu) - menu = menu or self.current - local index = itable_find(menu.items, function(item) return item.value == value end) - self:delete_index(index) -end - ----@param menu? MenuStack -function Menu:prev(menu) - menu = menu or self.current - menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1) - self:scroll_to_index(menu.selected_index, menu, true) -end - ----@param menu? MenuStack -function Menu:next(menu) - menu = menu or self.current - menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items) - self:scroll_to_index(menu.selected_index, menu, true) -end - -function Menu:back() - if self.opts.on_back then - self.opts.on_back() - if self.is_closed then return end - end - - local menu = self.current - local parent = menu.parent_menu - - if parent then - menu.selected_index = nil - self.current = parent - self:update_dimensions() - self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) - self.opacity = 1 -- in case tween above canceled fade in animation - else - self:close() - end -end - ----@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean} -function Menu:open_selected_item(opts) - opts = opts or {} - local menu = self.current - if menu.selected_index then - local item = menu.items[menu.selected_index] - -- Is submenu - if item.items then - self.current = item - if opts.preselect_submenu_item then - item.selected_index = #item.items > 0 and 1 or nil - end - self:update_dimensions() - self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) - self.opacity = 1 -- in case tween above canceled fade in animation - else - self.callback(item.value) - if not item.keep_open and not opts.keep_open then self:close() end - end - end -end - -function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end -function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end -function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end - -function Menu:on_display() self:update_dimensions() end -function Menu:on_prop_fullormaxed() self:update_content_dimensions() end - -function Menu:on_global_mbtn_left_down() - if self.proximity_raw == 0 then - self.drag_data = {{y = cursor.y, time = mp.get_time()}} - self.current.fling = nil - else - if cursor.x < self.ax then self:back() - else self:close() end - end -end - -function Menu:fling_distance() - local first, last = self.drag_data[1], self.drag_data[#self.drag_data] - if mp.get_time() - last.time > 0.05 then return 0 end - for i = #self.drag_data - 1, 1, -1 do - local drag = self.drag_data[i] - if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end - end - return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10 -end - -function Menu:on_global_mbtn_left_up() - if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then - self:select_item_below_cursor() - self:open_selected_item({preselect_submenu_item = false}) - end - if self.is_dragging then - local distance = self:fling_distance() - if math.abs(distance) > 50 then - self.current.fling = { - y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time, - easing = ease_out_quart, duration = 0.5, update_cursor = true, - } - end - end - self.is_dragging = false - self.drag_data = nil -end - - -function Menu:on_global_mouse_move() - self.mouse_nav = true - if self.drag_data then - self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10 - local distance = self.drag_data[#self.drag_data].y - cursor.y - if distance ~= 0 then self:set_scroll_by(distance) end - self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()} - end - if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor() - else self.current.selected_index = nil end - request_render() -end - -function Menu:on_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end -function Menu:on_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end - -function Menu:on_pgup() - local menu = self.current - local items_per_page = round((menu.height / self.scroll_step) * 0.4) - local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page - menu.selected_index = clamp(1, paged_index, #menu.items) - if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end -end - -function Menu:on_pgdwn() - local menu = self.current - local items_per_page = round((menu.height / self.scroll_step) * 0.4) - local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page - menu.selected_index = clamp(1, paged_index, #menu.items) - if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end -end - -function Menu:on_home() - self.current.selected_index = math.min(1, #self.current.items) - if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end -end - -function Menu:on_end() - self.current.selected_index = #self.current.items - if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end -end - -function Menu:add_key_binding(key, name, fn, flags) - self.key_bindings[#self.key_bindings + 1] = name - mp.add_forced_key_binding(key, name, fn, flags) -end - -function Menu:enable_key_bindings() - -- The `mp.set_key_bindings()` method would be easier here, but that - -- doesn't support 'repeatable' flag, so we are stuck with this monster. - self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable') - self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable') - self:add_key_binding('left', 'menu-back1', self:create_key_action('back')) - self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('shift+right', 'menu-select-soft1', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('shift+mbtn_left', 'menu-select-soft', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back')) - self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back')) - self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect')) - self:add_key_binding('shift+enter', 'menu-select-alt5', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('shift+kp_enter', 'menu-select-alt6', self:create_key_action('open_selected_item_soft')) - self:add_key_binding('esc', 'menu-close', self:create_key_action('close')) - self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup'), 'repeatable') - self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn'), 'repeatable') - self:add_key_binding('home', 'menu-home', self:create_key_action('on_home')) - self:add_key_binding('end', 'menu-end', self:create_key_action('on_end')) -end - -function Menu:disable_key_bindings() - for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end - self.key_bindings = {} -end - -function Menu:create_key_action(name) - return function(...) - self.mouse_nav = false - self:maybe(name, ...) - end -end - -function Menu:render() - local update_cursor = false - for _, menu in ipairs(self.all) do - if menu.fling then - update_cursor = update_cursor or menu.fling.update_cursor or false - local time_delta = state.render_last_time - menu.fling.time - local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1)) - self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu) - if progress < 1 then request_render() else menu.fling = nil end - end - end - if update_cursor then self:select_item_below_cursor() end - - local ass = assdraw.ass_new() - local opacity = options.menu_opacity * self.opacity - local spacing = self.item_padding - local icon_size = self.font_size - - function draw_menu(menu, x, y, opacity) - local ax, ay, bx, by = x, y, x + menu.width, y + menu.height - local draw_title = menu.is_root and menu.title - local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')' - local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1 - local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step) - local selected_index = menu.selected_index or -1 - -- remove menu_opacity to start off with full opacity, but still decay for parent menus - local text_opacity = opacity / options.menu_opacity - - -- Background - ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, { - color = bg, opacity = opacity, radius = 4, - }) - - for index = start_index, end_index, 1 do - local item = menu.items[index] - local next_item = menu.items[index + 1] - local is_highlighted = selected_index == index or item.active - local next_is_active = next_item and next_item.active - local next_is_highlighted = selected_index == index + 1 or next_is_active - - if not item then break end - - local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1) - local item_by = item_ay + self.item_height - local item_center_y = item_ay + (self.item_height / 2) - local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil - local content_ax, content_bx = ax + spacing, bx - spacing - local font_color = item.active and fgt or bgt - local shadow_color = item.active and fg or bg - - -- Separator - local separator_ay = item.separator and item_by - 1 or item_by - local separator_by = item_by + (item.separator and 2 or 1) - if is_highlighted then separator_ay = item_by + 1 end - if next_is_highlighted then separator_by = item_by end - if separator_by - separator_ay > 0 and item_by < by then - ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, { - color = fg, opacity = opacity * (item.separator and 0.08 or 0.06), - }) - end - - -- Highlight - local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0) - if highlight_opacity > 0 then - ass:rect(ax + 2, item_ay, bx - 2, item_by, { - radius = 2, color = fg, opacity = highlight_opacity * text_opacity, - clip = item_clip, - }) - end - - -- Icon - if item.icon then - local x, y = content_bx - (icon_size / 2), item_center_y - if item.icon == 'spinner' then - ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8}) - else - ass:icon(x, y, icon_size * 1.5, item.icon, { - color = font_color, opacity = text_opacity, clip = item_clip, - shadow = 1, shadow_color = shadow_color, - }) - end - content_bx = content_bx - icon_size - spacing - end - - local title_cut_x = content_bx - if item.hint_width > 0 then - -- controls title & hint clipping proportional to the ratio of their widths - local title_content_ratio = item.title_width / (item.title_width + item.hint_width) - title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio - + (item.title_width > 0 and spacing / 2 or 0)) - end - - -- Hint - if item.hint then - item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint) - local clip = '\\clip(' .. title_cut_x .. ',' .. - math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')' - ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, { - size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip, - shadow = 1, shadow_color = shadow_color, - }) - end - - -- Title - if item.title then - item.ass_safe_title = item.ass_safe_title or ass_escape(item.title) - local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ',' - .. title_cut_x .. ',' .. math.min(item_by, by) .. ')' - ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, { - size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2, - opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip, - shadow = 1, shadow_color = shadow_color, - }) - end - end - - -- Menu title - if draw_title then - local title_ay = ay - self.item_height - local title_height = self.item_height - 3 - menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title) - - -- Background - ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, { - color = fg, opacity = opacity * 0.8, radius = 2, - }) - ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', { - size = 80, color = bg, opacity = opacity * 0.1, - }) - - -- Title - ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, { - size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity, - clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')', - }) - end - - -- Scrollbar - if menu.scroll_height > 0 then - local groove_height = menu.height - 2 - local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40) - local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height)) - ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8}) - end - end - - -- Main menu - draw_menu(self.current, self.ax, self.ay, opacity) - - -- Parent menus - local parent_menu = self.current.parent_menu - local parent_offset_x = self.ax - local parent_opacity_factor = options.menu_parent_opacity - local menu_gap = 2 - - while parent_menu do - parent_offset_x = parent_offset_x - parent_menu.width - menu_gap - draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity) - parent_opacity_factor = parent_opacity_factor * parent_opacity_factor - parent_menu = parent_menu.parent_menu - end - - -- Selected menu - local selected_menu = self.current.items[self.current.selected_index] - - if selected_menu and selected_menu.items then - draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity) - end - - return ass -end - -return Menu |