1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
-- 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()
-- Ink size of a string: how much space the glyphs actually cover.
-- Use this to CENTRE or RIGHT-ALIGN text, never to advance a cursor.
function M.measure(cr, s)
cairo_text_extents(cr, s, extents)
return extents.width, extents.height
end
-- How far the cursor moves after drawing a string. Use this to lay out runs of
-- text left to right.
--
-- Not measure(): that returns the INK width, which ignores leading and
-- trailing spaces because a space carries no ink. Stepping a cursor by the ink
-- width collapses the gaps, and " / " measures 6px of ink against a 14px
-- advance, so a date drawn in segments came out as "16 /SEP /2026" with each
-- slash jammed into the next glyph.
function M.advance(cr, s)
cairo_text_extents(cr, s, extents)
return extents.x_advance
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
|