-- Clock: hour stacked over minute, weekday, slashed date. -- -- Shape follows idea2.png; every colour comes from the palette. -- -- Fluid: every size and offset derives from the rect it is handed, and the -- date block pins to the bottom so the slack in a tall cell falls between the -- numerals and the date rather than piling up under everything. A widget is -- given a rectangle and should fill it, whatever shape the grid makes it. local card = require 'lib.card' local M = {} local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end function M.draw(cr, rect, colors) local inner = card.card(cr, rect, colors) local x = inner.x local bottom = inner.y + inner.h -- The date block is sized first, because the numerals get whatever is left. -- Both scale off the WIDTH: height changes with the grid, and a widget that -- shrinks its type every time its cell gets shorter reads as broken next to -- its neighbours. Width is also what actually constrains the date line, -- which runs edge to edge. local day_size = clamp(inner.w * 0.055, 12, 26) local date_size = clamp(inner.w * 0.050, 11, 24) local date_band = day_size * 1.6 + date_size * 1.8 -- Two stacked numerals fill the space above the date band. -- -- The divisor is derived, not tuned: the first baseline sits at 0.78 of the -- size (cap height) and the second a further 0.92 below it, so the pair -- occupies size * 1.70. Dividing the available height by that is what makes -- the numerals actually reach the date band instead of leaving a gap; the -- earlier flat 0.46 left a fifth of the band empty. -- -- Whichever of height and width binds first wins, so a wide short cell is -- limited by its height and a narrow tall one by its width, and neither -- overflows. -- The width cap is the font's, measured rather than guessed: two Oswald -- digits are 1.00x the font size wide (measured at size 100: '00' is 100.0px, -- the widest pair '88' is 98.0px), so the size a width w permits is w itself, -- less a little breathing room at the card edges. local avail = (bottom - date_band) - inner.y local size = math.min(avail / 1.70, inner.w * 0.92) -- A narrow tall cell is width-bound, which leaves the numerals floating well -- above the date. Centre the pair in the space they cannot fill instead of -- hanging them from the top, so the slack sits above and below them equally. local slack = avail - size * 1.70 if slack < 0 then slack = 0 end card.font(cr, card.FONT_CLOCK, size, true) card.rgba(cr, colors.body) -- Baselines. Cairo's y is the baseline, so the first sits one cap-height -- down; 0.78 of the font size approximates cap height for Noto Sans and -- avoids measuring every frame. local y1 = inner.y + slack / 2 + size * 0.78 -- Tight leading, as in the mockup: the numerals nearly touch. local y2 = y1 + size * 0.92 -- Centred horizontally: measure(), not advance(), because ink width is what -- centring wants. Oswald is condensed, so '10' and '03' differ in width and -- each line is centred on its own measurement rather than a shared one. local function centred(s, baseline) local w = card.measure(cr, s) card.text(cr, inner.x + (inner.w - w) / 2, baseline, s) end centred(os.date('%H'), y1) centred(os.date('%M'), y2) -- Weekday and date sit against the bottom edge, not at a fixed drop from the -- numerals. That is what puts the slack of a tall cell in the middle of the -- card instead of leaving a dead band under the date. local dy = bottom - date_size * 0.3 local wy = dy - date_size * 1.8 card.font(cr, card.FONT_UI, day_size, true) card.rgba(cr, colors.body) card.text(cr, x, wy, os.date('%A'):upper()) -- Date as "16 / SEP / 2026". The slashes take the dimmer label colour so the -- numerals read first, which is what gives the mockup's date line its rhythm. card.font(cr, card.FONT_UI, date_size, true) local parts = { { os.date('%d'), colors.value }, { ' / ', colors.label }, { os.date('%b'):upper(), colors.value }, { ' / ', colors.label }, { os.date('%Y'), colors.value }, } local dx = x for _, p in ipairs(parts) do card.rgba(cr, p[2]) card.text(cr, dx, dy, p[1]) -- advance(), not measure(): the ink width of ' / ' omits its spaces. dx = dx + card.advance(cr, p[1]) end end return M