local base = require('wibox.widget.base') local dpi = require('beautiful').xresources.apply_dpi local gcolor = require('gears.color') local gshape = require('gears.shape') local gtable = require('gears.table') local lgi = require('lgi') local cairo = lgi.cairo local wibox = require('wibox') local element = { mt = {} } function element:layout(_, width, height) if self._private.widget then return { base.place_widget_at(self._private.widget, 0, 0, width, height) } end end function element:fit(context, width, height) local w, h = 0, 0 if self._private.widget then w, h = base.fit_widget(self, context, self._private.widget, width, height) end return w, h end function element:get_widget() return self._private.widget end function element:on_hover() self:connect_signal('mouse::enter', function() self.bg = '#0ffff033' self.border_color = '#0ffff099' end) self:connect_signal('mouse::leave', function() self.bg = gcolor.transparent self.border_color = gcolor.transparent end) self:connect_signal('button::press', function() self.bg = '#0ffff088' self.border_color = '#0ffff0dd' end) self:connect_signal('button::release', function() self.bg = '#0ffff033' self.border_color = '#0ffff099' end) end ---Get the cairo extents for any text with its give size and font ---@param font string A font ---@param font_size number Font size ---@param text string Text to get the extent for ---@param args table Additional arguments ---@return userdata cairo.Extent local function cairo_text_extents(font, font_size, text, args) local surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) local cr = cairo.Context(surface) cr:select_font_face(font, cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) cr:set_font_size(font_size) cr:set_antialias(cairo.Antialias.BEST) return cr:text_extents(text) end local function split_string(str, max_width) local line1 = '' local line2 = '' local line1_width = 0 local line2_width = 0 local font = 'JetBrainsMono Nerd Font' local font_size = dpi(16) local font_args = { cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD } for word in str:gmatch('%S+') do local word_width = cairo_text_extents(font, font_size, word, font_args).width if line1_width + word_width < max_width then line1 = line1 .. word .. ' ' line1_width = line1_width + word_width else line2 = line2 .. word .. ' ' line2_width = line2_width + word_width end end return line1, line2 end ---This function takes any text and uses cairo to draw an outline and a shadow ---It also wraps the text correctly if max_width would be violated. It only uses two lines for wraping ---the rest is cut off. ---@param text string Text to be changed ---@param max_width number max width the text won't go over ---@return cairo.Surface cairo_surface manupulated text as a cairo surface ---@return table `width`,`height` The surface dimensions local function outlined_text(text, max_width) local font = 'JetBrainsMono Nerd Font' local font_size = dpi(16) local spacing = dpi(5) local margin = dpi(5) max_width = max_width - (margin * 2) local shadow_offset_x, shadow_offset_y = 1, 1 local font_args = { cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD } -- Get the dimensions from the text local extents = cairo_text_extents(font, font_size, text, font_args) -- if its bigger it needs special treatment if extents.width > max_width then local line1, line2 = split_string(text, max_width) -- Get the dimensions for both lines local extents1 = cairo_text_extents(font, font_size, line1, font_args) local extents2 = cairo_text_extents(font, font_size, line2, font_args) -- The surface width will be the biggest of the two lines local s_width = extents1.width if extents1.width < extents2.width then s_width = extents2.width end -- Create a new surface based on the widest line, and both line's height + the spacing between them and the shadow offset local surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, s_width + shadow_offset_x, extents1.height + extents2.height + spacing + (shadow_offset_y * 3)) local cr = cairo.Context(surface) -- Create the font with best antialias cr:select_font_face(font, cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) cr:set_font_size(font_size) cr:set_antialias(cairo.Antialias.BEST) -- To center both lines get the surface center then substract half the line width local text_x = s_width / 2 - ((extents1.width) / 2) local text_x2 = s_width / 2 - ((extents2.width) / 2) -- This makes the first text to be blow the main text cr:set_operator(cairo.Operator.OVER) -- Draw the text shadow cr:move_to(text_x + shadow_offset_x, -extents1.y_bearing + shadow_offset_y) cr:set_source_rgba(0, 0, 0, 0.5) cr:show_text(line1) cr:set_operator(cairo.Operator.OVER) -- Draw the second shadow cr:move_to(text_x2 + shadow_offset_x, extents1.height + extents2.height + spacing + shadow_offset_y) cr:set_source_rgba(0, 0, 0, 0.5) cr:show_text(line2) -- Draw the first and second line cr:move_to(text_x, -extents1.y_bearing) cr:set_source_rgb(1, 1, 1) cr:text_path(line1) cr:move_to(text_x2, extents1.height + extents2.height + spacing) cr:text_path(line2) -- Color it and set the stroke cr:fill_preserve() cr:set_source_rgb(0, 0, 0) cr:set_line_width(0.1) cr:stroke() return surface, { width = extents.width, height = extents1.height + extents2.height + spacing } else -- The size is the dimension from above the if local surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, extents.width, extents.height + shadow_offset_y) local cr = cairo.Context(surface) -- Set the font, then draw the text and its stroke cr:select_font_face(font, cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) cr:set_font_size(font_size) -- This makes the first text to be blow the main text cr:set_operator(cairo.Operator.OVER) -- Draw the text shadow cr:move_to(-extents.x_bearing + shadow_offset_x, -extents.y_bearing + shadow_offset_y) cr:set_source_rgba(0, 0, 0, 0.5) cr:show_text(text) cr:move_to(-extents.x_bearing, -extents.y_bearing) cr:set_source_rgb(1, 1, 1) cr:text_path(text) cr:fill_preserve() cr:set_source_rgb(0, 0, 0) cr:set_line_width(0.1) cr:stroke() return surface, { width = extents.width, height = extents.height } end end function element.new(args) args = args or {} local text_img, size = outlined_text(args.label, args.width) local w = base.make_widget_from_value(wibox.widget { { { { { image = args.icon, resize = true, clip_shape = gshape.rounded_rect, valign = 'top', halign = 'center', id = 'icon_role', forced_width = args.icon_size, forced_height = args.icon_size, widget = wibox.widget.imagebox, }, widget = wibox.container.margin, top = dpi(5), left = dpi(20), right = dpi(20), bottom = dpi(5), }, { image = text_img, resize = false, valign = 'bottom', halign = 'center', widget = wibox.widget.imagebox, }, spacing = dpi(10), layout = wibox.layout.align.vertical, }, valign = 'center', halign = 'center', widget = wibox.container.place, }, fg = '#ffffff', bg = gcolor.transparent, border_color = gcolor.transparent, border_width = dpi(2), shape = gshape.rounded_rect, forced_width = args.width, forced_height = args.height, width = args.width, height = args.height, exec = args.exec, icon_size = args.icon_size, icon = args.icon, label = args.label, widget = wibox.container.background, }) assert(w, 'No widget returned') gtable.crush(w, element, true) w:on_hover() return w end function element.mt:__call(...) return element.new(...) end return setmetatable(element, element.mt)