-- Cairo drawing primitives shared by every widget. -- -- A widget is handed a rect and these helpers; it never computes its own -- position and never touches the palette directly. local M = {} -- Cairo's font API tops out at CAIRO_FONT_WEIGHT_BOLD, so a Black face cannot -- be requested by weight. Passing the family name that fontconfig registers for -- it ("Noto Sans Black") with NORMAL weight does select it: measured at size -- 90, "13" is 100.0px wide as Black against 94.0px as Noto Sans Bold. M.FONT_MONO = 'Inconsolata Nerd Font' M.FONT_UI = 'Noto Sans' M.FONT_HEAVY = 'Noto Sans Black' function M.font(cr, family, size, bold) cairo_select_font_face(cr, family, CAIRO_FONT_SLANT_NORMAL, bold and CAIRO_FONT_WEIGHT_BOLD or CAIRO_FONT_WEIGHT_NORMAL) cairo_set_font_size(cr, size) end function M.rgba(cr, c, alpha) cairo_set_source_rgba(cr, c[1], c[2], c[3], alpha or c[4] or 1) end -- Text at (x, y), where y is the BASELINE, not the top of the glyphs. function M.text(cr, x, y, s) cairo_move_to(cr, x, y) cairo_show_text(cr, s) end -- Measured width and height of a string under the current font. -- One reused extents struct for every measurement, allocated at load. -- -- Not per call: cairo_text_extents_t:create() leaks. 5000 allocations grow -- Lua's heap by ~182KB that collectgarbage() never reclaims, and calling -- :destroy() on each one does NOT help (measured: same 182KB either way). -- Reusing a single struct costs 0KB. In a draw hook running every 2s with -- several measured strings per frame, the per-call version bleeds memory for as -- long as conky is up, which is exactly the kind of fault a screenshot cannot -- show. -- -- Safe because the draw hook is single-threaded and each measure() consumes the -- values before the next call overwrites them. local extents = cairo_text_extents_t:create() function M.measure(cr, s) cairo_text_extents(cr, s, extents) return extents.width, extents.height end -- Right-aligned text: x is the RIGHT edge. function M.text_right(cr, x, y, s) local w = M.measure(cr, s) M.text(cr, x - w, y, s) end -- A rounded-rectangle path. Does not paint; the caller fills or strokes, so one -- path can serve both a fill and its border. function M.rounded_path(cr, x, y, w, h, r) -- Clamp the radius: a radius over half the shorter side makes the arcs -- overlap and Cairo draws a pinched, bowtie-looking shape. local m = math.min(w, h) / 2 if r > m then r = m end cairo_new_path(cr) cairo_arc(cr, x + w - r, y + r, r, -math.pi / 2, 0) cairo_arc(cr, x + w - r, y + h - r, r, 0, math.pi / 2) cairo_arc(cr, x + r, y + h - r, r, math.pi / 2, math.pi) cairo_arc(cr, x + r, y + r, r, math.pi, math.pi * 1.5) cairo_close_path(cr) end -- A card: filled rounded rect with a hairline border, matching the mockups. -- Returns the inner rect so a widget can lay out against padded bounds. function M.card(cr, rect, colors, pad) pad = pad or 18 M.rounded_path(cr, rect.x, rect.y, rect.w, rect.h, 14) M.rgba(cr, colors.surface, 0.55) cairo_fill_preserve(cr) -- 1px hairline. Cairo strokes astride the path, so a width of 1 on an integer -- coordinate straddles two pixel rows and renders as a soft 2px line; the -- cards read as thin outlines in the mockup, so keep it sub-pixel-crisp by -- stroking at 1 and accepting the AA rather than offsetting by 0.5, which -- would misalign the fill. M.rgba(cr, colors.border, 0.9) cairo_set_line_width(cr, 1) cairo_stroke(cr) return { x = rect.x + pad, y = rect.y + pad, w = rect.w - pad * 2, h = rect.h - pad * 2 } end -- A small-caps section label, as used across both mockups. function M.label(cr, x, y, s, colors) M.font(cr, M.FONT_MONO, 13, false) M.rgba(cr, colors.label) M.text(cr, x, y, s:upper()) end return M