From 7368d0d782360287e61c0304169718422a14c36c Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 10 Jul 2026 15:03:57 +0200 Subject: [PATCH] toolkit: make font metrics runtime-pluggable (Font interface) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the last pre-1.0 model-edge item. Introduces a Font interface (Advance/Height/Draw) with a scalable built-in bitmap font: SetFont( NewBitmapFont(2)) doubles all text ('retina' rendering), and every widget re-lays-out because GlyphHeight/GlyphAdvance are now read from the active font at draw time instead of being compile-time constants. BREAKING (pre-1.0): GlyphHeight and GlyphAdvance become functions, as do the six metric-derived dimensions (CardHeaderH, CardFooterH, DatePickerFieldH, DiffLineH, FormFieldLabelH, TimelineEventH) — a const can't call a function. All 197 internal call sites updated; consumers append () when upgrading. The default (scale-1) bitmap renders pixel-identical to before, so no golden regressions. 100% coverage retained; go vet clean; 6 arches + wasm build. Co-Authored-By: Claude Opus 4.8 --- README.md | 13 +++-- actionrow.go | 2 +- actionrow_test.go | 6 +- avatar.go | 2 +- badge.go | 6 +- badge_test.go | 2 +- banner.go | 2 +- breadcrumbs.go | 6 +- breadcrumbs_test.go | 6 +- button.go | 2 +- calendar.go | 8 +-- card.go | 27 +++++---- card_test.go | 8 +-- chatbubble.go | 4 +- chatbubble_test.go | 4 +- check.go | 2 +- chip.go | 6 +- chip_test.go | 2 +- colorchooser.go | 2 +- datepicker.go | 6 +- datepicker_test.go | 4 +- dialog.go | 2 +- diff.go | 8 +-- diff_test.go | 10 ++-- dropdown.go | 2 +- dropzone.go | 2 +- entry.go | 6 +- expander.go | 2 +- font.go | 130 +++++++++++++++++++++++++++++++++----------- font_test.go | 40 +++++++------- fontmetrics_test.go | 81 +++++++++++++++++++++++++++ formfield.go | 6 +- formfield_test.go | 20 +++---- headerbar.go | 6 +- iconbutton.go | 2 +- kbd.go | 4 +- label.go | 8 +-- list.go | 2 +- markdownview.go | 20 +++---- menu.go | 4 +- notebook.go | 2 +- notification.go | 2 +- pagination.go | 4 +- pagination_test.go | 22 ++++---- popover.go | 2 +- popover_test.go | 4 +- progress.go | 2 +- progresscircle.go | 2 +- radio.go | 2 +- rating.go | 2 +- searchentry.go | 6 +- searchentry_test.go | 12 ++-- skeleton.go | 2 +- spinbutton.go | 6 +- splitbutton.go | 4 +- stat.go | 4 +- stat_test.go | 16 +++--- statusbar.go | 2 +- steps.go | 2 +- steps_test.go | 4 +- table.go | 6 +- textview.go | 8 +-- timeline.go | 17 +++--- timeline_test.go | 20 +++---- toggle.go | 2 +- toolbar.go | 2 +- tooltip.go | 2 +- treeview.go | 2 +- v04_test.go | 4 +- viewswitcher.go | 2 +- widget_test.go | 6 +- 71 files changed, 402 insertions(+), 246 deletions(-) create mode 100644 fontmetrics_test.go diff --git a/README.md b/README.md index bddddff..661110a 100644 --- a/README.md +++ b/README.md @@ -158,11 +158,14 @@ Widget coverage is now materially complete versus GTK 4 + DaisyUI 4. The remaining pre-1.0 work is around the *edges* of the widget model, not the widget catalogue: -- **Font family plumbing** — the toolkit still ships one 5×7 bitmap - font. A `Font` interface (`GlyphAdvance`, `GlyphHeight`, `Draw`) - so a caller can plug a larger bitmap or a hand-rasterised - TrueType at boot. Unblocks `FontChooser` (long-standing deferral) - + retina-size Label rendering. +- ~~**Font family plumbing**~~ — **done (v0.20)**: a `Font` interface + (`Advance`, `Height`, `Draw`) + a scalable built-in bitmap font. + `SetFont(NewBitmapFont(2))` doubles all text ("retina"), and every + widget re-lays-out because metrics are now read at draw time. + **Breaking:** `GlyphHeight` / `GlyphAdvance` and the metric-derived + dimensions (`CardHeaderH`, `DatePickerFieldH`, `DiffLineH`, + `FormFieldLabelH`, `TimelineEventH`, `CardFooterH`) are now + **functions** — append `()` at call sites when upgrading. - ~~**First-class drag-and-drop event kinds**~~ — **done (v0.16)**: `EventDragStart` / `EventDragMove` / `EventDragLeave` / `EventDrop`, plus a `DragSource` / `DropTarget` interface pair and diff --git a/actionrow.go b/actionrow.go index d0ea480..4d059cd 100644 --- a/actionrow.go +++ b/actionrow.go @@ -79,7 +79,7 @@ func (a *ActionRow) Draw(p painter.Painter, theme *Theme) { ty := r.Y + ActionRowPadY DrawText(p, textX, ty, a.Title, theme.OnSurface) if a.Subtitle != "" { - sy := ty + GlyphHeight + ActionRowSubtitleGap + sy := ty + GlyphHeight() + ActionRowSubtitleGap DrawText(p, textX, sy, a.Subtitle, dimInk(theme)) } diff --git a/actionrow_test.go b/actionrow_test.go index 83fb907..4c92bed 100644 --- a/actionrow_test.go +++ b/actionrow_test.go @@ -65,7 +65,7 @@ func TestActionRowDrawBodyAndDivider(t *testing.T) { } // Title text present in OnSurface. found := false - for y := ActionRowPadY; y < ActionRowPadY+GlyphHeight && !found; y++ { + for y := ActionRowPadY; y < ActionRowPadY+GlyphHeight() && !found; y++ { for x := ActionRowPadX; x < ActionRowPadX+TextWidth("Title") && !found; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { found = true @@ -114,9 +114,9 @@ func TestActionRowDrawWithSubtitle(t *testing.T) { a.Draw(newP(buf, w), theme) // Subtitle ink is drawn in dimInk tone below the title. - sy := ActionRowPadY + GlyphHeight + ActionRowSubtitleGap + sy := ActionRowPadY + GlyphHeight() + ActionRowSubtitleGap found := false - for y := sy; y < sy+GlyphHeight && !found; y++ { + for y := sy; y < sy+GlyphHeight() && !found; y++ { for x := ActionRowPadX; x < ActionRowPadX+TextWidth("sub") && !found; x++ { if pixelAt(buf, w, x, y) == dimInk(theme) { found = true diff --git a/avatar.go b/avatar.go index a139762..1ee706a 100644 --- a/avatar.go +++ b/avatar.go @@ -77,6 +77,6 @@ func (a *Avatar) Draw(p painter.Painter, theme *Theme) { tw := TextWidth(a.Initials) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, a.Initials, accentInk(theme)) } diff --git a/badge.go b/badge.go index 565da75..26d76f3 100644 --- a/badge.go +++ b/badge.go @@ -17,7 +17,7 @@ import "github.com/go-widgets/painter" // // Auto-sizing: if the caller sets Bounds().W to 0, the first Draw() // resizes the Bounds to the text width plus BadgePadX on each side -// (plus GlyphHeight + BadgePadY on each side vertically if H is also 0). +// (plus GlyphHeight() + BadgePadY on each side vertically if H is also 0). // This spares the caller from having to compute glyph widths just to // paint a two-digit counter. A pre-sized Bounds is honoured verbatim // so a fixed-width layout column doesn't shift when the digit count @@ -53,7 +53,7 @@ func (b *Badge) Draw(p painter.Painter, theme *Theme) { if r.W == 0 { r.W = TextWidth(b.Text) + 2*BadgePadX if r.H == 0 { - r.H = GlyphHeight + 2*BadgePadY + r.H = GlyphHeight() + 2*BadgePadY } b.SetBounds(r) } @@ -65,6 +65,6 @@ func (b *Badge) Draw(p painter.Painter, theme *Theme) { fillRect(p, r.X+r.W-1, r.Y+1, 1, r.H-2, theme.Accent) tw := TextWidth(b.Text) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, b.Text, theme.Background) } diff --git a/badge_test.go b/badge_test.go index 9999ce2..2bc5a20 100644 --- a/badge_test.go +++ b/badge_test.go @@ -16,7 +16,7 @@ func TestBadgeAutoSizesWhenWZero(t *testing.T) { b.Draw(newP(makeSurface(100, 40), 100), theme) got := b.Bounds() wantW := TextWidth("42") + 2*BadgePadX - wantH := GlyphHeight + 2*BadgePadY + wantH := GlyphHeight() + 2*BadgePadY if got.W != wantW { t.Fatalf("auto-sized W = %d, want %d", got.W, wantW) } diff --git a/banner.go b/banner.go index 9b356bb..2f914e5 100644 --- a/banner.go +++ b/banner.go @@ -53,7 +53,7 @@ func (b *Banner) buttonRect() (Rect, bool) { } r := b.Bounds() btnW := TextWidth(b.ButtonLabel) + 2*BannerButtonPadX - btnH := GlyphHeight + 2*BannerPadY + btnH := GlyphHeight() + 2*BannerPadY return Rect{ X: r.X + r.W - btnW - BannerPadX, Y: r.Y + (r.H-btnH)/2, diff --git a/breadcrumbs.go b/breadcrumbs.go index 209c725..00195da 100644 --- a/breadcrumbs.go +++ b/breadcrumbs.go @@ -39,12 +39,12 @@ func NewBreadcrumbs(segments []string) *Breadcrumbs { // Draw paints each segment followed by a separator (except after the // last one). Segments are vertically centred inside Bounds when -// Bounds.H exceeds GlyphHeight, otherwise they anchor at Bounds.Y. +// Bounds.H exceeds GlyphHeight(), otherwise they anchor at Bounds.Y. func (b *Breadcrumbs) Draw(p painter.Painter, theme *Theme) { r := b.Bounds() ty := r.Y - if r.H > GlyphHeight { - ty = r.Y + (r.H-GlyphHeight)/2 + if r.H > GlyphHeight() { + ty = r.Y + (r.H-GlyphHeight())/2 } x := r.X n := len(b.Segments) diff --git a/breadcrumbs_test.go b/breadcrumbs_test.go index 1cffda9..18c6710 100644 --- a/breadcrumbs_test.go +++ b/breadcrumbs_test.go @@ -100,13 +100,13 @@ func TestBreadcrumbsDrawMultiSegmentPaintsChevron(t *testing.T) { } } -// Tight bounds (Bounds.H == GlyphHeight): the centring branch is +// Tight bounds (Bounds.H == GlyphHeight()): the centring branch is // skipped and text anchors at Bounds.Y. func TestBreadcrumbsDrawTightBoundsSkipsCentring(t *testing.T) { - const w, h = 40, GlyphHeight + 2 + var w, h = 40, GlyphHeight() + 2 theme := DefaultLight() b := NewBreadcrumbs([]string{"Hi"}) - b.SetBounds(Rect{X: 0, Y: 0, W: 40, H: GlyphHeight}) + b.SetBounds(Rect{X: 0, Y: 0, W: 40, H: GlyphHeight()}) buf := makeSurface(w, h) b.Draw(newP(buf, w), theme) // The centring branch skipped -> ink starts at y = Bounds.Y = 0. diff --git a/button.go b/button.go index 5f49913..7926168 100644 --- a/button.go +++ b/button.go @@ -98,7 +98,7 @@ func (b *Button) Draw(p painter.Painter, theme *Theme) { if b.Label != "" { tw := TextWidth(b.Label) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, b.Label, ink) } } diff --git a/calendar.go b/calendar.go index 1d40397..411442d 100644 --- a/calendar.go +++ b/calendar.go @@ -131,7 +131,7 @@ func (c *Calendar) Draw(p painter.Painter, theme *Theme) { // Header: month / year. hdr := monthName(c.Month) + " " + itoa(c.Year) hx := r.X + (r.W-TextWidth(hdr))/2 - hy := r.Y + (CalendarHeaderH-GlyphHeight)/2 + hy := r.Y + (CalendarHeaderH-GlyphHeight())/2 DrawText(p, hx, hy, hdr, theme.OnSurface) // Weekday row. weekdayY := r.Y + CalendarHeaderH @@ -142,7 +142,7 @@ func (c *Calendar) Draw(p painter.Painter, theme *Theme) { // Day grid. first := WeekdayOfFirst(c.Year, c.Month) dim := DaysInMonth(c.Year, c.Month) - gridY := weekdayY + GlyphHeight + 4 + gridY := weekdayY + GlyphHeight() + 4 for d := 1; d <= dim; d++ { idx := first + d - 1 col := idx % 7 @@ -160,7 +160,7 @@ func (c *Calendar) Draw(p painter.Painter, theme *Theme) { } fillRect(p, cx, cy, CalendarCellW, CalendarCellH, bg) txt := itoa(d) - DrawText(p, cx+(CalendarCellW-TextWidth(txt))/2, cy+(CalendarCellH-GlyphHeight)/2, txt, ink) + DrawText(p, cx+(CalendarCellW-TextWidth(txt))/2, cy+(CalendarCellH-GlyphHeight())/2, txt, ink) } } @@ -169,7 +169,7 @@ func (c *Calendar) OnEvent(ev Event) { if ev.Kind != EventClick { return } - gridY := CalendarHeaderH + GlyphHeight + 4 + gridY := CalendarHeaderH + GlyphHeight() + 4 if ev.Y < gridY { return } diff --git a/card.go b/card.go index 3e55ff9..5586f36 100644 --- a/card.go +++ b/card.go @@ -43,15 +43,18 @@ const ( // CardPadY is the vertical inset for the body text above the first // line + between the footer text and its strip border. CardPadY = 6 - // CardHeaderH is the height of the header strip when Title != "". - CardHeaderH = GlyphHeight + 2*CardPadY - // CardFooterH is the height of the footer strip when Footer != "". - CardFooterH = GlyphHeight + 2*CardPadY // CardLineSpacing is the extra vertical gap inserted between two // body lines so successive glyph rows don't touch. CardLineSpacing = 2 ) +// CardHeaderH is the height of the header strip when Title != "" (a function, +// as it derives from the active font's GlyphHeight). +func CardHeaderH() int { return GlyphHeight() + 2*CardPadY } + +// CardFooterH is the height of the footer strip when Footer != "". +func CardFooterH() int { return GlyphHeight() + 2*CardPadY } + // NewCard constructs a Card with the given title, body + footer. // Any of the three may be "" to skip that zone. func NewCard(title, body, footer string) *Card { @@ -69,25 +72,25 @@ func (c *Card) Draw(p painter.Painter, theme *Theme) { bodyTop := r.Y if c.Title != "" { - fillRect(p, r.X, r.Y, r.W, CardHeaderH, theme.SurfaceAlt) - ty := r.Y + (CardHeaderH-GlyphHeight)/2 + fillRect(p, r.X, r.Y, r.W, CardHeaderH(), theme.SurfaceAlt) + ty := r.Y + (CardHeaderH()-GlyphHeight())/2 DrawText(p, r.X+CardPadX, ty, c.Title, theme.OnSurface) // Divider between header and body. - fillRect(p, r.X, r.Y+CardHeaderH, r.W, 1, theme.Border) - bodyTop = r.Y + CardHeaderH + 1 + fillRect(p, r.X, r.Y+CardHeaderH(), r.W, 1, theme.Border) + bodyTop = r.Y + CardHeaderH() + 1 } if c.Footer != "" { - footerY := r.Y + r.H - CardFooterH - fillRect(p, r.X, footerY, r.W, CardFooterH, theme.SurfaceAlt) - ty := footerY + (CardFooterH-GlyphHeight)/2 + footerY := r.Y + r.H - CardFooterH() + fillRect(p, r.X, footerY, r.W, CardFooterH(), theme.SurfaceAlt) + ty := footerY + (CardFooterH()-GlyphHeight())/2 DrawText(p, r.X+CardPadX, ty, c.Footer, theme.OnSurface) // Divider between body and footer. fillRect(p, r.X, footerY-1, r.W, 1, theme.Border) } if c.Body != "" { - lineH := GlyphHeight + CardLineSpacing + lineH := GlyphHeight() + CardLineSpacing y := bodyTop + CardPadY for _, ln := range strings.Split(c.Body, "\n") { DrawText(p, r.X+CardPadX, y, ln, theme.OnSurface) diff --git a/card_test.go b/card_test.go index bee9209..93be38e 100644 --- a/card_test.go +++ b/card_test.go @@ -40,14 +40,14 @@ func TestCardDrawAllZonesPopulated(t *testing.T) { t.Fatalf("footer strip pixel = %+v, want SurfaceAlt", pixelAt(buf, w, w-4, h-2)) } // Body area (between header and footer) should show the Surface fill. - bodyY := CardHeaderH + 4 + bodyY := CardHeaderH() + 4 if pixelAt(buf, w, w-4, bodyY) != theme.Surface { t.Fatalf("body area pixel = %+v, want Surface", pixelAt(buf, w, w-4, bodyY)) } // The body ink lands somewhere inside the body area — at least one // pixel painted in OnSurface confirms the split-on-\n loop ran. painted := 0 - for y := CardHeaderH + 1; y < h-CardFooterH-1; y++ { + for y := CardHeaderH() + 1; y < h-CardFooterH()-1; y++ { for x := 0; x < w; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { painted++ @@ -98,7 +98,7 @@ func TestCardDrawEmptyBodyPaintsNoLines(t *testing.T) { c.Draw(newP(buf, w), theme) // Body area — between header divider and footer divider — no ink. inked := 0 - for y := CardHeaderH + 2; y < h-CardFooterH-2; y++ { + for y := CardHeaderH() + 2; y < h-CardFooterH()-2; y++ { for x := 4; x < w-4; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { inked++ @@ -139,7 +139,7 @@ func TestCardDrawSingleLineBody(t *testing.T) { buf := makeSurface(w, h) c.Draw(newP(buf, w), theme) painted := 0 - for y := CardPadY; y < CardPadY+GlyphHeight; y++ { + for y := CardPadY; y < CardPadY+GlyphHeight(); y++ { for x := 0; x < w; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { painted++ diff --git a/chatbubble.go b/chatbubble.go index 6066d10..83e1bf1 100644 --- a/chatbubble.go +++ b/chatbubble.go @@ -35,7 +35,7 @@ const ( // Sizing: the bubble grows to fit the widest text line plus 2*PadX, // capped at ChatBubbleMaxW so a runaway paste doesn't spill the // widget's Bounds. Height is len(lines) * lineH + 2*PadY where lineH -// = GlyphHeight + ChatBubbleLineSpacing. +// = GlyphHeight() + ChatBubbleLineSpacing. // // ChatBubble is a passive display widget — it does not intercept // input (HitTest / OnEvent stay as Base defaults). A caller that @@ -89,7 +89,7 @@ func (c *ChatBubble) Draw(p painter.Painter, theme *Theme) { if bubbleW > ChatBubbleMaxW { bubbleW = ChatBubbleMaxW } - lineH := GlyphHeight + ChatBubbleLineSpacing + lineH := GlyphHeight() + ChatBubbleLineSpacing bubbleH := len(lines)*lineH - ChatBubbleLineSpacing + 2*ChatBubblePadY var bx int diff --git a/chatbubble_test.go b/chatbubble_test.go index 44b0bf4..acbd18e 100644 --- a/chatbubble_test.go +++ b/chatbubble_test.go @@ -122,7 +122,7 @@ func TestChatBubbleDrawMultiLine(t *testing.T) { buf := makeSurface(w, h) c.Draw(newP(buf, w), theme) - // The third line ("CCC", width=18) sits at y = padY + 2*(GlyphHeight+LineSpacing) + // The third line ("CCC", width=18) sits at y = padY + 2*(GlyphHeight()+LineSpacing) // = 6 + 2*9 = 24. Its glyphs are 7 rows tall so ink should appear // somewhere in rows 24..30. Just verify some OnSurface ink exists past // the first-line region. @@ -232,7 +232,7 @@ func TestChatBubbleDrawEmptyText(t *testing.T) { c.SetBounds(Rect{X: 0, Y: 0, W: w, H: h}) buf := makeSurface(w, h) c.Draw(newP(buf, w), theme) - // Empty text: bubble is 20x19 (2*PadX x GlyphHeight+2*PadY). Interior + // Empty text: bubble is 20x19 (2*PadX x GlyphHeight()+2*PadY). Interior // pixel at (10,10) is inside the SurfaceAlt fill. if got := pixelAt(buf, w, 10, 10); got != theme.SurfaceAlt { t.Fatalf("empty bubble interior = %+v, want SurfaceAlt", got) diff --git a/check.go b/check.go index 34af3c5..15d99be 100644 --- a/check.go +++ b/check.go @@ -49,7 +49,7 @@ func (c *CheckButton) Draw(p painter.Painter, theme *Theme) { } } // Label to the right of the box, vertically centred on glyph row. - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 DrawText(p, r.X+checkBoxSize+4, textY, c.Label, theme.OnBackground) } diff --git a/chip.go b/chip.go index 39f09dd..595278b 100644 --- a/chip.go +++ b/chip.go @@ -17,7 +17,7 @@ import "github.com/go-widgets/painter" // Auto-sizing follows Badge's convention: if Bounds().W is zero the // first Draw() sets W to the text width plus ChipPadX on each side // (plus the close slot's ChipCloseGap + ChipCloseW when Closable is -// true), and H to GlyphHeight + 2*ChipPadY when it is also zero. A +// true), and H to GlyphHeight() + 2*ChipPadY when it is also zero. A // pre-sized Bounds is honoured verbatim so a fixed-width layout row // does not shift when the chip label changes. type Chip struct { @@ -67,14 +67,14 @@ func (c *Chip) Draw(p painter.Painter, theme *Theme) { r.W += ChipCloseGap + ChipCloseW } if r.H == 0 { - r.H = GlyphHeight + 2*ChipPadY + r.H = GlyphHeight() + 2*ChipPadY } c.SetBounds(r) } fillRect(p, r.X, r.Y, r.W, r.H, theme.SurfaceAlt) strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) tx := r.X + ChipPadX - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, c.Text, theme.OnSurface) if c.Closable { cx := r.X + r.W - ChipPadX - ChipCloseW + (ChipCloseW-TextWidth("x"))/2 diff --git a/chip_test.go b/chip_test.go index 654d8e8..66dd97b 100644 --- a/chip_test.go +++ b/chip_test.go @@ -48,7 +48,7 @@ func TestChipAutoSizesWhenWZero(t *testing.T) { c.Draw(newP(makeSurface(80, 40), 80), theme) got := c.Bounds() wantW := TextWidth("hi") + 2*ChipPadX - wantH := GlyphHeight + 2*ChipPadY + wantH := GlyphHeight() + 2*ChipPadY if got.W != wantW { t.Fatalf("auto-sized W = %d, want %d", got.W, wantW) } diff --git a/colorchooser.go b/colorchooser.go index 072b521..3c44a51 100644 --- a/colorchooser.go +++ b/colorchooser.go @@ -49,7 +49,7 @@ func (c *ColorChooser) Draw(p painter.Painter, theme *Theme) { for i, ch := range [3]string{"R", "G", "B"} { y := r.Y + ColorChooserChannelPadY + i*ColorChooserChannelH labelX := r.X + 2 - DrawText(p, labelX, y+(ColorChooserChannelH-GlyphHeight)/2, ch, theme.OnSurface) + DrawText(p, labelX, y+(ColorChooserChannelH-GlyphHeight())/2, ch, theme.OnSurface) trackX := r.X + ColorChooserPadX + 12 trackY := y + ColorChooserChannelH/2 - 2 trackW := channelW - 12 diff --git a/datepicker.go b/datepicker.go index 4501571..b46defa 100644 --- a/datepicker.go +++ b/datepicker.go @@ -24,7 +24,7 @@ type DatePicker struct { } // DatePickerFieldH is the pixel height of the closed field. -const DatePickerFieldH = GlyphHeight + 10 +func DatePickerFieldH() int { return GlyphHeight() + 10 } // NewDatePicker builds a DatePicker initialised to (year, month, day). func NewDatePicker(year, month, day int) *DatePicker { @@ -55,7 +55,7 @@ func (dp *DatePicker) Text() string { // calendar width below the field. Six week-rows is the worst case. func (dp *DatePicker) PopoverBounds() Rect { r := dp.Bounds() - h := CalendarHeaderH + GlyphHeight + 4 + 6*CalendarCellH + 4 + h := CalendarHeaderH + GlyphHeight() + 4 + 6*CalendarCellH + 4 return Rect{X: r.X, Y: r.Y + r.H, W: 7 * CalendarCellW, H: h} } @@ -65,7 +65,7 @@ func (dp *DatePicker) Draw(p painter.Painter, theme *Theme) { r := dp.Bounds() fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 DrawText(p, r.X+6, textY, dp.Text(), theme.OnSurface) dp.drawIcon(p, r, theme) if dp.Open { diff --git a/datepicker_test.go b/datepicker_test.go index 2c0c445..62d81d8 100644 --- a/datepicker_test.go +++ b/datepicker_test.go @@ -12,7 +12,7 @@ import ( // top-left so popover coordinate math is exercised. func newTestDatePicker() *DatePicker { dp := NewDatePicker(2026, 7, 4) - dp.SetBounds(Rect{X: 0, Y: 0, W: 160, H: DatePickerFieldH}) + dp.SetBounds(Rect{X: 0, Y: 0, W: 160, H: DatePickerFieldH()}) return dp } @@ -75,7 +75,7 @@ func TestDatePickerPickDayFiresOnChange(t *testing.T) { first := WeekdayOfFirst(2026, 7) idx := first + 15 - 1 col, row := idx%7, idx/7 - gridY := CalendarHeaderH + GlyphHeight + 4 + gridY := CalendarHeaderH + GlyphHeight() + 4 calX := col*CalendarCellW + CalendarCellW/2 calY := gridY + row*CalendarCellH + CalendarCellH/2 // widget-local = calendar-local + (popover origin - field origin) diff --git a/dialog.go b/dialog.go index 3424fc9..dca5386 100644 --- a/dialog.go +++ b/dialog.go @@ -67,7 +67,7 @@ func (d *Dialog) Draw(p painter.Painter, theme *Theme) { strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) // Title bar. fillRect(p, r.X, r.Y, r.W, DialogTitleH, theme.SurfaceAlt) - titleY := r.Y + (DialogTitleH-GlyphHeight)/2 + titleY := r.Y + (DialogTitleH-GlyphHeight())/2 DrawText(p, r.X+8, titleY, d.Title, theme.OnSurface) // Content. if d.Content != nil { diff --git a/diff.go b/diff.go index 32f494f..2bedb96 100644 --- a/diff.go +++ b/diff.go @@ -47,7 +47,7 @@ const ( // DiffLineH is the vertical stride between successive lines: one // glyph tall plus two pixels of separation. -const DiffLineH = GlyphHeight + 2 +func DiffLineH() int { return GlyphHeight() + 2 } // DiffPadX is the horizontal padding between the widget's outer // border and the leading prefix glyph. @@ -91,7 +91,7 @@ func (d *Diff) Draw(p painter.Painter, theme *Theme) { r := d.Bounds() fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) for i, line := range d.Lines { - y := r.Y + DiffPadY + i*DiffLineH + y := r.Y + DiffPadY + i*DiffLineH() fill := theme.Surface ink := theme.OnSurface prefix := " " @@ -105,9 +105,9 @@ func (d *Diff) Draw(p painter.Painter, theme *Theme) { ink = diffRemovedInk prefix = "-" } - fillRect(p, r.X+1, y, r.W-2, DiffLineH, fill) + fillRect(p, r.X+1, y, r.W-2, DiffLineH(), fill) DrawText(p, r.X+DiffPadX, y, prefix, ink) - DrawText(p, r.X+DiffPadX+GlyphAdvance, y, line.Text, ink) + DrawText(p, r.X+DiffPadX+GlyphAdvance(), y, line.Text, ink) } strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) } diff --git a/diff_test.go b/diff_test.go index 867eaa3..a6df0d0 100644 --- a/diff_test.go +++ b/diff_test.go @@ -65,7 +65,7 @@ func TestDiffDrawAddedLinePaintsGreenTint(t *testing.T) { d.SetBounds(Rect{X: 0, Y: 0, W: 80, H: 30}) buf := makeSurface(w, h) d.Draw(newP(buf, w), theme) - // Row 0 lives at y = DiffPadY .. DiffPadY+DiffLineH. + // Row 0 lives at y = DiffPadY .. DiffPadY+DiffLineH(). // Sample a pixel well inside the row band, past the outer stroke. if pixelAt(buf, w, w-3, DiffPadY+1) != diffAddedFill { t.Fatalf("Added row tint = %+v, want diffAddedFill", @@ -119,12 +119,12 @@ func TestDiffDrawMixedKinds(t *testing.T) { buf := makeSurface(w, h) d.Draw(newP(buf, w), theme) // Row 1 = Added. - yAdded := DiffPadY + 1*DiffLineH + 1 + yAdded := DiffPadY + 1*DiffLineH() + 1 if pixelAt(buf, w, w-3, yAdded) != diffAddedFill { t.Fatalf("row 1 tint = %+v, want Added", pixelAt(buf, w, w-3, yAdded)) } // Row 2 = Removed. - yRemoved := DiffPadY + 2*DiffLineH + 1 + yRemoved := DiffPadY + 2*DiffLineH() + 1 if pixelAt(buf, w, w-3, yRemoved) != diffRemovedFill { t.Fatalf("row 2 tint = %+v, want Removed", pixelAt(buf, w, w-3, yRemoved)) } @@ -155,9 +155,9 @@ func TestDiffDrawPaintsPrefixGlyphs(t *testing.T) { {"- prefix", diffRemovedInk}, } for i, w2 := range wants { - y0 := DiffPadY + i*DiffLineH + y0 := DiffPadY + i*DiffLineH() found := false - for y := y0; y < y0+GlyphHeight && !found; y++ { + for y := y0; y < y0+GlyphHeight() && !found; y++ { for x := DiffPadX; x < DiffPadX+5; x++ { if pixelAt(buf, w, x, y) == w2.ink { found = true diff --git a/dropdown.go b/dropdown.go index ca545cf..4ac0c86 100644 --- a/dropdown.go +++ b/dropdown.go @@ -49,7 +49,7 @@ func (d *DropDown) Draw(p painter.Painter, theme *Theme) { r := d.Bounds() fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 DrawText(p, r.X+6, textY, d.Current(), theme.OnSurface) // ▼ chevron on the right edge to signal a drop-down. The wide base // sits on the top row and rows narrow moving down to the point, diff --git a/dropzone.go b/dropzone.go index f589e56..a45ea09 100644 --- a/dropzone.go +++ b/dropzone.go @@ -94,7 +94,7 @@ func (d *DropZone) Draw(p painter.Painter, theme *Theme) { } tw := TextWidth(d.Prompt) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, d.Prompt, theme.OnSurface) } diff --git a/entry.go b/entry.go index a9cf26f..efd2be5 100644 --- a/entry.go +++ b/entry.go @@ -39,11 +39,11 @@ func (e *Entry) Draw(p painter.Painter, theme *Theme) { } fillRoundRect(p, r.X, r.Y, r.W, r.H, buttonRadius, theme.Surface) strokeRoundRect(p, r.X, r.Y, r.W, r.H, buttonRadius, border) - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 DrawText(p, r.X+4, textY, e.Text, theme.OnSurface) if e.Focused { - cx := r.X + 4 + e.Cursor*GlyphAdvance - fillRect(p, cx, textY-1, 1, GlyphHeight+2, theme.OnSurface) + cx := r.X + 4 + e.Cursor*GlyphAdvance() + fillRect(p, cx, textY-1, 1, GlyphHeight()+2, theme.OnSurface) } } diff --git a/expander.go b/expander.go index f35808f..ab9e6dd 100644 --- a/expander.go +++ b/expander.go @@ -54,7 +54,7 @@ func (e *Expander) Draw(p painter.Painter, theme *Theme) { fillRect(p, cx+2-t, cy-t, 1, 1+2*t, theme.OnSurface) } } - textY := r.Y + (ExpanderHeaderH-GlyphHeight)/2 + textY := r.Y + (ExpanderHeaderH-GlyphHeight())/2 DrawText(p, r.X+16, textY, e.Label, theme.OnSurface) if e.Expanded && e.Content != nil { body := Rect{X: r.X, Y: r.Y + ExpanderHeaderH, W: r.W, H: r.H - ExpanderHeaderH} diff --git a/font.go b/font.go index 6bcffe8..944fa75 100644 --- a/font.go +++ b/font.go @@ -16,14 +16,75 @@ import "github.com/go-widgets/painter" // bit 6 = bottom row). The rendering loop below mirrors the dock's // drawTextClipped column/row decode. -// GlyphHeight is the per-glyph vertical extent in pixels. -const GlyphHeight = 7 +// Font is the toolkit's text metrics + rendering abstraction. Widgets lay +// themselves out against the ACTIVE font's metrics (via GlyphHeight / +// GlyphAdvance) and paint text through DrawText, so swapping the active font +// with SetFont rescales the whole UI's typography without touching any widget. +// +// - Advance is the horizontal step from one glyph origin to the next. +// - Height is the glyph box height. +// - Draw paints text left-to-right at (x, y) in the given ink. +// +// All built-in widgets assume a monospace font (fixed Advance), which keeps +// grid-aligned layout math trivial. +type Font interface { + Advance() int + Height() int + Draw(p painter.Painter, x, y int, text string, ink RGBA) +} + +// baseGlyphW / baseGlyphH are the unscaled dimensions of the built-in 5x7 +// bitmap (5 columns of body + 1 of spacing horizontally, 7 rows tall). +const ( + baseGlyphAdvance = 6 + baseGlyphHeight = 7 +) + +// bitmapFont renders the built-in font5x7 table at an integer Scale: each lit +// bit becomes a Scale×Scale block, so Scale=2 doubles the type size ("retina" +// text) while staying a pure-integer, dependency-free rasteriser. +type bitmapFont struct{ Scale int } + +// Advance is the scaled horizontal step per glyph. +func (f *bitmapFont) Advance() int { return baseGlyphAdvance * f.Scale } + +// Height is the scaled glyph box height. +func (f *bitmapFont) Height() int { return baseGlyphHeight * f.Scale } + +// NewBitmapFont returns the built-in 5x7 font scaled by the given integer +// factor (clamped to at least 1). SetFont(NewBitmapFont(2)) doubles all text. +func NewBitmapFont(scale int) Font { + if scale < 1 { + scale = 1 + } + return &bitmapFont{Scale: scale} +} + +// defaultFont is the unscaled 5x7 bitmap — the toolkit's out-of-the-box font. +var defaultFont = &bitmapFont{Scale: 1} + +// activeFont is the font every widget currently lays out + renders against. +var activeFont Font = defaultFont -// GlyphAdvance is the horizontal step from one glyph's origin to the -// next (5px glyph body + 1px inter-glyph spacing). All glyphs share -// the same advance so layout stays grid-aligned even with proportional -// shapes. -const GlyphAdvance = 6 +// SetFont makes f the active font. A nil f restores the built-in default. All +// subsequent layout (GlyphHeight / GlyphAdvance) and DrawText use it. +func SetFont(f Font) { + if f == nil { + f = defaultFont + } + activeFont = f +} + +// CurrentFont returns the active font. +func CurrentFont() Font { return activeFont } + +// GlyphHeight is the active font's glyph box height. It is a function (not a +// const) so widgets re-read it after SetFont; layout dimensions that derive +// from it are likewise functions. +func GlyphHeight() int { return activeFont.Height() } + +// GlyphAdvance is the active font's horizontal step from one glyph to the next. +func GlyphAdvance() int { return activeFont.Advance() } // font5x7 is the 5-column x 7-row bitmap font table. Each entry is one // glyph: 5 bytes, one per column, low 7 bits encode the rows from top @@ -120,45 +181,52 @@ var font5x7 = map[byte][5]byte{ } // TextWidth returns the pixel width that DrawText would occupy if it -// rendered text. Every character (known or unknown) consumes one -// GlyphAdvance slot so callers can pre-size text containers from -// len(text) alone -- this matches the dock's textWidth helper. -func TextWidth(text string) int { return GlyphAdvance * len(text) } - -// DrawText paints text left-to-right starting at (x, y) in widget- -// local coordinates. Each glyph is rendered into a 5x7 cell whose -// origin is (x + k*GlyphAdvance, y) for the k-th rune. Unknown -// characters render as a blank but still advance the cursor so -// column alignment is preserved. -// -// On a *painter.PixelPainter (the WUI + GUI back-end path) DrawText -// uses toolkit's own 60+ glyph 5x7 bitmap font by writing one pixel -// per lit bit. On any other painter (a *painter.CellPainter for a -// TUI, an SvgPainter for vector output) DrawText delegates to the -// painter's own Text primitive — a CellPainter maps one rune per -// cell + gets terminal-native text, an SvgPainter emits . -// -// Pixels are written as opaque ink (alpha forced to 0xFF unless the -// caller's ink already carries an alpha) with per-pixel clipping so -// glyphs that overflow the painter degrade gracefully. +// rendered text in the active font. Every character (known or unknown) +// consumes one GlyphAdvance slot so callers can pre-size text containers from +// len(text) alone. +func TextWidth(text string) int { return GlyphAdvance() * len(text) } + +// DrawText paints text left-to-right starting at (x, y) in widget-local +// coordinates, using the active font (see SetFont). It is a thin wrapper over +// the active Font's Draw so every widget's text rendering follows a font swap. func DrawText(p painter.Painter, x, y int, text string, ink RGBA) { + activeFont.Draw(p, x, y, text, ink) +} + +// Draw paints text with the bitmap font. On a *painter.PixelPainter (the WUI + +// GUI path) each lit bit of the 5x7 glyph becomes a Scale×Scale block whose +// origin is (x + k*Advance, y) for the k-th rune, so the font scales cleanly. +// On any other painter (a *painter.CellPainter for a TUI, an SvgPainter for +// vector output) it delegates to the painter's own Text primitive — one rune +// per cell / a native — where the pixel scale is not meaningful. +// +// Unknown characters render blank but still advance the cursor so column +// alignment is preserved; pixels clip per-pixel so overflowing glyphs degrade +// gracefully. +func (f *bitmapFont) Draw(p painter.Painter, x, y int, text string, ink RGBA) { if _, isPixel := p.(*painter.PixelPainter); !isPixel { p.Text(x, y, text, ink) return } + adv := f.Advance() for k := 0; k < len(text); k++ { bits, ok := font5x7[text[k]] if !ok { continue } - gx := x + k*GlyphAdvance + gx := x + k*adv for col := 0; col < 5; col++ { cb := bits[col] - for row := 0; row < GlyphHeight; row++ { + for row := 0; row < baseGlyphHeight; row++ { if cb&(1< GlyphHeight the text is vertically centred; otherwise +// Bounds.H > GlyphHeight() the text is vertically centred; otherwise // it lands at Bounds.Y. func (l *Label) Draw(p painter.Painter, theme *Theme) { r := l.Bounds() ty := r.Y - if r.H > GlyphHeight { - ty += (r.H - GlyphHeight) / 2 + if r.H > GlyphHeight() { + ty += (r.H - GlyphHeight()) / 2 } DrawText(p, r.X, ty, l.Text, theme.OnSurface) } diff --git a/list.go b/list.go index 6fa5963..d91c7b8 100644 --- a/list.go +++ b/list.go @@ -48,7 +48,7 @@ func (l *ListBox) Draw(p painter.Painter, theme *Theme) { } fillRect(p, r.X, y, r.W, l.RowHeight, bg) // Vertically centre the 7-px glyph inside the row. - textY := y + (l.RowHeight-GlyphHeight)/2 + textY := y + (l.RowHeight-GlyphHeight())/2 DrawText(p, r.X+4, textY, item, ink) } } diff --git a/markdownview.go b/markdownview.go index e3e49bb..e80d827 100644 --- a/markdownview.go +++ b/markdownview.go @@ -134,13 +134,13 @@ func wordWrap(text string, maxChars int) []string { } // mdLineH is the baseline-to-baseline advance for a rendered text line. -const mdLineH = GlyphHeight + 4 +func mdLineH() int { return GlyphHeight() + 4 } // Draw lays the parsed blocks top-to-bottom within Bounds. func (m *MarkdownView) Draw(p painter.Painter, theme *Theme) { r := m.Bounds() fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) - maxChars := (r.W - 8) / GlyphAdvance + maxChars := (r.W - 8) / GlyphAdvance() y := r.Y + 4 for _, b := range parseMarkdown(m.Source) { y = m.drawBlock(p, theme, b, r, maxChars, y) @@ -153,29 +153,29 @@ func (m *MarkdownView) drawBlock(p painter.Painter, theme *Theme, b mdBlock, r R case mdHeading: DrawText(p, r.X+4, y, b.text, theme.Accent) if b.level <= 2 { // rule under H1/H2 - fillRect(p, r.X+4, y+GlyphHeight+1, TextWidth(b.text), 1, theme.Accent) + fillRect(p, r.X+4, y+GlyphHeight()+1, TextWidth(b.text), 1, theme.Accent) y += 2 } - return y + mdLineH + 2 + return y + mdLineH() + 2 case mdBullet: DrawText(p, r.X+4, y, "•", theme.OnSurface) // "•" for i, ln := range wordWrap(b.text, maxChars-2) { - DrawText(p, r.X+4+2*GlyphAdvance, y+i*mdLineH, ln, theme.OnSurface) + DrawText(p, r.X+4+2*GlyphAdvance(), y+i*mdLineH(), ln, theme.OnSurface) } - return y + len(wordWrap(b.text, maxChars-2))*mdLineH + return y + len(wordWrap(b.text, maxChars-2))*mdLineH() case mdCode: codeLines := strings.Split(b.text, "\n") - bandH := len(codeLines) * mdLineH + bandH := len(codeLines) * mdLineH() fillRect(p, r.X+4, y-2, r.W-8, bandH+4, theme.SurfaceAlt) for i, ln := range codeLines { - DrawText(p, r.X+8, y+i*mdLineH, ln, theme.OnSurface) + DrawText(p, r.X+8, y+i*mdLineH(), ln, theme.OnSurface) } return y + bandH + 4 default: // mdParagraph wrapped := wordWrap(b.text, maxChars) for i, ln := range wrapped { - DrawText(p, r.X+4, y+i*mdLineH, ln, theme.OnSurface) + DrawText(p, r.X+4, y+i*mdLineH(), ln, theme.OnSurface) } - return y + len(wrapped)*mdLineH + 2 + return y + len(wrapped)*mdLineH() + 2 } } diff --git a/menu.go b/menu.go index 16e9f50..0d96f9f 100644 --- a/menu.go +++ b/menu.go @@ -69,7 +69,7 @@ func (m *Menu) Draw(p painter.Painter, theme *Theme) { } else if i == m.Hover { ink = theme.Background // hovered row: invert ink } - textY := y + (MenuRowH-GlyphHeight)/2 + textY := y + (MenuRowH-GlyphHeight())/2 DrawText(p, r.X+8, textY, it.Label, ink) if it.Submenu != nil { // ▶ chevron on the right edge to signal a nested menu. @@ -221,7 +221,7 @@ func (b *MenuBar) Draw(p painter.Painter, theme *Theme) { } tw := TextWidth(name) textX := ix + (iw-tw)/2 - textY := r.Y + (MenuBarH-GlyphHeight)/2 + textY := r.Y + (MenuBarH-GlyphHeight())/2 DrawText(p, textX, textY, name, ink) } } diff --git a/notebook.go b/notebook.go index 443538e..4006fa0 100644 --- a/notebook.go +++ b/notebook.go @@ -54,7 +54,7 @@ func (n *Notebook) Draw(p painter.Painter, theme *Theme) { // Label centred in the tab. tw := TextWidth(tab.Label) textX := tx + (NotebookTabWidth-tw)/2 - textY := r.Y + (NotebookTabStripH-GlyphHeight)/2 + textY := r.Y + (NotebookTabStripH-GlyphHeight())/2 DrawText(p, textX, textY, tab.Label, theme.OnSurface) if i == n.Active { // Accent underline so the active tab reads as selected. diff --git a/notification.go b/notification.go index 95f118e..4e89e37 100644 --- a/notification.go +++ b/notification.go @@ -60,7 +60,7 @@ func (n *Notification) Show(text string) { n.Life = NotificationLife r := n.Bounds() r.W = TextWidth(text) + 2*NotificationPadX - r.H = GlyphHeight + 2*NotificationPadY + r.H = GlyphHeight() + 2*NotificationPadY n.SetBounds(r) } diff --git a/pagination.go b/pagination.go index c61c68b..547a36d 100644 --- a/pagination.go +++ b/pagination.go @@ -97,7 +97,7 @@ func (pg *Pagination) drawStep(p painter.Painter, theme *Theme, x, y int, label ink = theme.Border } tx := x + (PaginationBtnW-TextWidth(label))/2 - ty := y + (PaginationBtnH-GlyphHeight)/2 + ty := y + (PaginationBtnH-GlyphHeight())/2 DrawText(p, tx, ty, label, ink) } @@ -117,7 +117,7 @@ func (pg *Pagination) drawSlot(p painter.Painter, theme *Theme, x, y int, slot p fillRect(p, x, y, PaginationBtnW, PaginationBtnH, fill) strokeRect(p, x, y, PaginationBtnW, PaginationBtnH, theme.Border) tx := x + (PaginationBtnW-TextWidth(label))/2 - ty := y + (PaginationBtnH-GlyphHeight)/2 + ty := y + (PaginationBtnH-GlyphHeight())/2 DrawText(p, tx, ty, label, ink) } diff --git a/pagination_test.go b/pagination_test.go index a678af6..981a692 100644 --- a/pagination_test.go +++ b/pagination_test.go @@ -176,12 +176,12 @@ func TestPaginationDrawPrevDisabledInkIsBorder(t *testing.T) { p.SetBounds(Rect{X: 0, Y: 0, W: w, H: h}) buf := makeSurface(w, h) p.Draw(newP(buf, w), theme) - // The "<" glyph centers around ~x = (PaginationBtnW - GlyphAdvance)/2. + // The "<" glyph centers around ~x = (PaginationBtnW - GlyphAdvance())/2. // Look for any Border-coloured ink in the prev-button interior // rows (excluding the outer stroke) to confirm disabled tone. found := false - ty := (PaginationBtnH - GlyphHeight) / 2 - for y := ty; y < ty+GlyphHeight && !found; y++ { + ty := (PaginationBtnH - GlyphHeight()) / 2 + for y := ty; y < ty+GlyphHeight() && !found; y++ { for x := 1; x < PaginationBtnW-1; x++ { if pixelAt(buf, w, x, y) == theme.Border && y > 0 && y < h-1 { found = true @@ -204,9 +204,9 @@ func TestPaginationDrawNextDisabledInkIsBorder(t *testing.T) { buf := makeSurface(w, h) p.Draw(newP(buf, w), theme) nextX := (PaginationBtnW + PaginationGap) * 4 // prev + 3 nums = 4 - ty := (PaginationBtnH - GlyphHeight) / 2 + ty := (PaginationBtnH - GlyphHeight()) / 2 found := false - for y := ty; y < ty+GlyphHeight && !found; y++ { + for y := ty; y < ty+GlyphHeight() && !found; y++ { for x := nextX + 1; x < nextX+PaginationBtnW-1; x++ { if pixelAt(buf, w, x, y) == theme.Border && y > 0 && y < h-1 { found = true @@ -232,9 +232,9 @@ func TestPaginationDrawEllipsisSlot(t *testing.T) { // Left ellipsis is numeric slot 1 (0-based) → overall button // index 2 (prev + 1 numeric + this). ellX := (PaginationBtnW + PaginationGap) * 2 - ty := (PaginationBtnH - GlyphHeight) / 2 + ty := (PaginationBtnH - GlyphHeight()) / 2 found := false - for y := ty; y < ty+GlyphHeight && !found; y++ { + for y := ty; y < ty+GlyphHeight() && !found; y++ { for x := ellX + 1; x < ellX+PaginationBtnW-1; x++ { if pixelAt(buf, w, x, y) == theme.Border { found = true @@ -261,9 +261,9 @@ func TestPaginationDrawWithOnAccentExtra(t *testing.T) { p.Draw(newP(buf, w), theme) // Current = page 2 = numeric slot 1 → overall button index 2. numX := (PaginationBtnW + PaginationGap) * 2 - ty := (PaginationBtnH - GlyphHeight) / 2 + ty := (PaginationBtnH - GlyphHeight()) / 2 found := false - for y := ty; y < ty+GlyphHeight && !found; y++ { + for y := ty; y < ty+GlyphHeight() && !found; y++ { for x := numX + 1; x < numX+PaginationBtnW-1; x++ { if pixelAt(buf, w, x, y) == custom { found = true @@ -288,9 +288,9 @@ func TestPaginationDrawWithoutOnAccentExtraFallsBack(t *testing.T) { buf := makeSurface(w, h) p.Draw(newP(buf, w), theme) numX := (PaginationBtnW + PaginationGap) * 2 - ty := (PaginationBtnH - GlyphHeight) / 2 + ty := (PaginationBtnH - GlyphHeight()) / 2 found := false - for y := ty; y < ty+GlyphHeight && !found; y++ { + for y := ty; y < ty+GlyphHeight() && !found; y++ { for x := numX + 1; x < numX+PaginationBtnW-1; x++ { if pixelAt(buf, w, x, y) == theme.Background { found = true diff --git a/popover.go b/popover.go index 9000bce..d6006f8 100644 --- a/popover.go +++ b/popover.go @@ -49,7 +49,7 @@ func (p *Popover) headerH() int { if p.Title == "" { return 0 } - return GlyphHeight + PopoverPadY + return GlyphHeight() + PopoverPadY } // childRect returns the surface-coordinate rect the child widget diff --git a/popover_test.go b/popover_test.go index 2f4312c..52623a2 100644 --- a/popover_test.go +++ b/popover_test.go @@ -111,7 +111,7 @@ func TestPopoverDrawVisibleWithTitle(t *testing.T) { p.Draw(newP(buf, 120), theme) // Look for the title ink (OnSurface) among the top rows. found := false - for y := 0; y < GlyphHeight+PopoverPadY+2 && !found; y++ { + for y := 0; y < GlyphHeight()+PopoverPadY+2 && !found; y++ { for x := 0; x < 120; x++ { if pixelAt(buf, 120, x, y) == theme.OnSurface { found = true @@ -123,7 +123,7 @@ func TestPopoverDrawVisibleWithTitle(t *testing.T) { t.Fatal("no OnSurface title glyph pixel found in header strip") } // Child bounds should be pushed down by the header height. - wantY := PopoverPadY + GlyphHeight + PopoverPadY + wantY := PopoverPadY + GlyphHeight() + PopoverPadY if child.Bounds().Y != wantY { t.Fatalf("Child.Bounds.Y = %d, want %d", child.Bounds().Y, wantY) } diff --git a/progress.go b/progress.go index d3ab2c0..2612e44 100644 --- a/progress.go +++ b/progress.go @@ -49,7 +49,7 @@ func (pb *ProgressBar) Draw(p painter.Painter, theme *Theme) { if pb.Label != "" { tw := TextWidth(pb.Label) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, pb.Label, theme.OnSurface) } } diff --git a/progresscircle.go b/progresscircle.go index e1d1183..a2e392d 100644 --- a/progresscircle.go +++ b/progresscircle.go @@ -120,7 +120,7 @@ func (pc *ProgressCircle) Draw(p painter.Painter, theme *Theme) { tw := TextWidth(caption) if inner.W > 0 && inner.H > 0 { tx := inner.X + (inner.W-tw)/2 - ty := inner.Y + (inner.H-GlyphHeight)/2 + ty := inner.Y + (inner.H-GlyphHeight())/2 DrawText(p, tx, ty, caption, theme.OnSurface) } } diff --git a/radio.go b/radio.go index f3a5b15..4163f89 100644 --- a/radio.go +++ b/radio.go @@ -42,7 +42,7 @@ func (r *RadioButton) Draw(p painter.Painter, theme *Theme) { if r.Checked { fillRect(p, b.X+3, boxY+3, radioBoxSize-6, radioBoxSize-6, theme.Accent) } - textY := b.Y + (b.H-GlyphHeight)/2 + textY := b.Y + (b.H-GlyphHeight())/2 DrawText(p, b.X+radioBoxSize+4, textY, r.Label, theme.OnBackground) } diff --git a/rating.go b/rating.go index 49ee9d0..aa5a902 100644 --- a/rating.go +++ b/rating.go @@ -73,7 +73,7 @@ func (r *Rating) Draw(p painter.Painter, theme *Theme) { fillRect(p, x, b.Y, RatingStarW, RatingStarW, fill) tw := TextWidth("*") tx := x + (RatingStarW-tw)/2 - ty := b.Y + (RatingStarW-GlyphHeight)/2 + ty := b.Y + (RatingStarW-GlyphHeight())/2 DrawText(p, tx, ty, "*", glyphInk) } } diff --git a/searchentry.go b/searchentry.go index 65f9968..4af8e22 100644 --- a/searchentry.go +++ b/searchentry.go @@ -55,15 +55,15 @@ func (s *SearchEntry) Draw(p painter.Painter, theme *Theme) { r := s.Bounds() fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 // Left prefix slot. - prefixX := r.X + SearchEntryPadX + (SearchEntryIconW-GlyphAdvance)/2 + prefixX := r.X + SearchEntryPadX + (SearchEntryIconW-GlyphAdvance())/2 DrawText(p, prefixX, textY, searchEntryPrefix, theme.OnSurface) // Middle text. DrawText(p, r.X+SearchEntryPadX+SearchEntryIconW, textY, s.Text, theme.OnSurface) // Right clear slot only when there is text to clear. if s.Text != "" { - clearX := r.X + r.W - SearchEntryPadX - SearchEntryIconW + (SearchEntryIconW-GlyphAdvance)/2 + clearX := r.X + r.W - SearchEntryPadX - SearchEntryIconW + (SearchEntryIconW-GlyphAdvance())/2 DrawText(p, clearX, textY, searchEntryClear, theme.Border) } } diff --git a/searchentry_test.go b/searchentry_test.go index fb51e12..7d1d8f3 100644 --- a/searchentry_test.go +++ b/searchentry_test.go @@ -39,9 +39,9 @@ func TestSearchEntryDrawEmptyNoClearIcon(t *testing.T) { // clear "x". The frame stroke itself lives on the four outermost // rows/columns; anything strictly interior in Border is the // affordance. There should be none. - textY := (h - GlyphHeight) / 2 + textY := (h - GlyphHeight()) / 2 interiorRight := w - SearchEntryPadX - SearchEntryIconW - for y := textY; y < textY+GlyphHeight; y++ { + for y := textY; y < textY+GlyphHeight(); y++ { for x := interiorRight; x < w-1; x++ { if pixelAt(buf, w, x, y) == theme.Border && y > 0 && y < h-1 { t.Fatalf("empty SearchEntry painted a clear affordance at (%d,%d)", x, y) @@ -61,10 +61,10 @@ func TestSearchEntryDrawWithTextPaintsClearIcon(t *testing.T) { s.Draw(newP(buf, w), theme) // Look inside the right icon slot's interior rows for at least // one Border-coloured pixel that isn't part of the outer stroke. - textY := (h - GlyphHeight) / 2 + textY := (h - GlyphHeight()) / 2 interiorLeft := w - SearchEntryPadX - SearchEntryIconW found := false - for y := textY; y < textY+GlyphHeight && !found; y++ { + for y := textY; y < textY+GlyphHeight() && !found; y++ { for x := interiorLeft; x < w-1; x++ { if pixelAt(buf, w, x, y) == theme.Border { found = true @@ -87,10 +87,10 @@ func TestSearchEntryDrawPaintsPrefixGlyph(t *testing.T) { buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) // Some OnSurface ink lands in the left icon slot's interior. - textY := (h - GlyphHeight) / 2 + textY := (h - GlyphHeight()) / 2 interiorRight := SearchEntryPadX + SearchEntryIconW found := false - for y := textY; y < textY+GlyphHeight && !found; y++ { + for y := textY; y < textY+GlyphHeight() && !found; y++ { for x := 1; x < interiorRight; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { found = true diff --git a/skeleton.go b/skeleton.go index 93f5727..25d39fd 100644 --- a/skeleton.go +++ b/skeleton.go @@ -44,7 +44,7 @@ type Skeleton struct { } // Skeleton sizing constants. Values chosen to line up with the toolkit's -// GlyphHeight so a SkeletonText row visually replaces a row of body +// GlyphHeight() so a SkeletonText row visually replaces a row of body // text without shifting the surrounding layout. const ( // SkeletonLineH is the pixel height of a single SkeletonText bar. diff --git a/spinbutton.go b/spinbutton.go index d398366..88512e0 100644 --- a/spinbutton.go +++ b/spinbutton.go @@ -54,7 +54,7 @@ func (s *SpinButton) Draw(p painter.Painter, theme *Theme) { strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) // Value text in the left portion. text := strconv.Itoa(s.Value) - textY := r.Y + (r.H-GlyphHeight)/2 + textY := r.Y + (r.H-GlyphHeight())/2 DrawText(p, r.X+4, textY, text, theme.OnSurface) // Two buttons on the right, vertically stacked. btnX := r.X + r.W - spinButtonW @@ -63,8 +63,8 @@ func (s *SpinButton) Draw(p painter.Painter, theme *Theme) { fillRect(p, btnX, r.Y+half, spinButtonW, r.H-half, theme.SurfaceAlt) strokeRect(p, btnX, r.Y, spinButtonW, half, theme.Border) strokeRect(p, btnX, r.Y+half, spinButtonW, r.H-half, theme.Border) - DrawText(p, btnX+5, r.Y+(half-GlyphHeight)/2, "+", theme.OnSurface) - DrawText(p, btnX+5, r.Y+half+(r.H-half-GlyphHeight)/2, "-", theme.OnSurface) + DrawText(p, btnX+5, r.Y+(half-GlyphHeight())/2, "+", theme.OnSurface) + DrawText(p, btnX+5, r.Y+half+(r.H-half-GlyphHeight())/2, "-", theme.OnSurface) } // OnEvent: click on the upper-right button increments; click on the diff --git a/splitbutton.go b/splitbutton.go index 0a90778..06382e2 100644 --- a/splitbutton.go +++ b/splitbutton.go @@ -70,13 +70,13 @@ func (s *SplitButton) Draw(p painter.Painter, theme *Theme) { fillRect(p, r.X+mainW, r.Y, 1, r.H, theme.Border) aw := TextWidth("v") ax := r.X + mainW + (SplitButtonArrowW-aw)/2 - ay := r.Y + (r.H-GlyphHeight)/2 + ay := r.Y + (r.H-GlyphHeight())/2 DrawText(p, ax, ay, "v", ink) } if s.Label != "" { tw := TextWidth(s.Label) tx := r.X + (mainW-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, s.Label, ink) } } diff --git a/stat.go b/stat.go index 02bdb83..9167f9f 100644 --- a/stat.go +++ b/stat.go @@ -108,12 +108,12 @@ func (s *Stat) Draw(p painter.Painter, theme *Theme) { titleY := r.Y + StatPadY DrawText(p, titleX, titleY, s.Title, dimInk(theme)) - valueY := titleY + GlyphHeight + StatTitleGap + valueY := titleY + GlyphHeight() + StatTitleGap DrawText(p, titleX, valueY, s.Value, theme.OnSurface) DrawText(p, titleX+1, valueY, s.Value, theme.OnSurface) if s.Change != "" { - changeY := valueY + GlyphHeight + StatValueGap + changeY := valueY + GlyphHeight() + StatValueGap DrawText(p, titleX, changeY, s.Change, statChangeInk(s.Trend, theme)) } diff --git a/stat_test.go b/stat_test.go index fd14e97..c14a741 100644 --- a/stat_test.go +++ b/stat_test.go @@ -105,9 +105,9 @@ func TestStatDrawAllFieldsUp(t *testing.T) { // Value row painted in OnSurface ink. '1' at column 0 has bits[0]=0x00, // but bits[1]=0x42 (row 1 lit) — so scan the value row for an OnSurface // pixel rather than asserting a specific coordinate. - valueY := StatPadY + GlyphHeight + StatTitleGap + valueY := StatPadY + GlyphHeight() + StatTitleGap inked := 0 - for y := valueY; y < valueY+GlyphHeight; y++ { + for y := valueY; y < valueY+GlyphHeight(); y++ { for x := StatPadX; x < w-StatPadX; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { inked++ @@ -118,7 +118,7 @@ func TestStatDrawAllFieldsUp(t *testing.T) { t.Fatal("value row painted 0 OnSurface pixels") } // Change row painted in StatUp green. '+' col 0 = 0x08 -> row 3 lit. - changeY := valueY + GlyphHeight + StatValueGap + changeY := valueY + GlyphHeight() + StatValueGap wantUp := RGBA{R: 50, G: 150, B: 80, A: 255} got := pixelAt(buf, w, StatPadX, changeY+3) if got != wantUp { @@ -135,7 +135,7 @@ func TestStatDrawChangeDownRed(t *testing.T) { s.SetBounds(Rect{X: 0, Y: 0, W: 120, H: 60}) buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) - changeY := StatPadY + GlyphHeight + StatTitleGap + GlyphHeight + StatValueGap + changeY := StatPadY + GlyphHeight() + StatTitleGap + GlyphHeight() + StatValueGap wantDown := RGBA{R: 190, G: 60, B: 60, A: 255} // '-' col 0 bits[0]=0x08 -> row 3 lit. if got := pixelAt(buf, w, StatPadX, changeY+3); got != wantDown { @@ -153,7 +153,7 @@ func TestStatDrawChangeFlatDim(t *testing.T) { s.SetBounds(Rect{X: 0, Y: 0, W: 120, H: 60}) buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) - changeY := StatPadY + GlyphHeight + StatTitleGap + GlyphHeight + StatValueGap + changeY := StatPadY + GlyphHeight() + StatTitleGap + GlyphHeight() + StatValueGap wantDim := dimInk(theme) // '-' col 0 bits[0]=0x08 -> row 3 lit. if got := pixelAt(buf, w, StatPadX, changeY+3); got != wantDim { @@ -170,13 +170,13 @@ func TestStatDrawEmptyChangeSkipsRow(t *testing.T) { s.SetBounds(Rect{X: 0, Y: 0, W: 120, H: 60}) buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) - changeY := StatPadY + GlyphHeight + StatTitleGap + GlyphHeight + StatValueGap + changeY := StatPadY + GlyphHeight() + StatTitleGap + GlyphHeight() + StatValueGap upInk := RGBA{R: 50, G: 150, B: 80, A: 255} downInk := RGBA{R: 190, G: 60, B: 60, A: 255} // The change row's pixel band must have neither Up nor Down ink. // (Border ink also shouldn't appear inside the fill band, so scan // the full 5x7 glyph rows.) - for y := changeY; y < changeY+GlyphHeight; y++ { + for y := changeY; y < changeY+GlyphHeight(); y++ { for x := 0; x < w; x++ { got := pixelAt(buf, w, x, y) if got == upInk || got == downInk { @@ -202,7 +202,7 @@ func TestStatDrawValueBoldDoubleDraw(t *testing.T) { s.SetBounds(Rect{X: 0, Y: 0, W: 60, H: 60}) buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) - valueY := StatPadY + GlyphHeight + StatTitleGap + valueY := StatPadY + GlyphHeight() + StatTitleGap // Row 0 of 'H' col 0 sits at (StatPadX, valueY). The +1 pass // puts the same row 0 lit pixel at (StatPadX+1, valueY). if got := pixelAt(buf, w, StatPadX, valueY); got != theme.OnSurface { diff --git a/statusbar.go b/statusbar.go index 79e21c3..ae89064 100644 --- a/statusbar.go +++ b/statusbar.go @@ -70,7 +70,7 @@ func (s *Statusbar) Draw(p painter.Painter, theme *Theme) { w = min } } - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, x+StatusbarPadX, ty, seg, theme.OnSurface) if i < n-1 { fillRect(p, x+w-1, r.Y+2, 1, r.H-4, theme.Border) diff --git a/steps.go b/steps.go index 989ba35..ca17f5f 100644 --- a/steps.go +++ b/steps.go @@ -87,7 +87,7 @@ func (s *Steps) Draw(p painter.Painter, theme *Theme) { num := strconv.Itoa(i + 1) tw := TextWidth(num) tx := x + (StepBoxW-tw)/2 - ty := y + (StepBoxH-GlyphHeight)/2 + ty := y + (StepBoxH-GlyphHeight())/2 DrawText(p, tx, ty, num, ink) if lab != "" { lw := TextWidth(lab) diff --git a/steps_test.go b/steps_test.go index 0fb9cf3..d4f41bc 100644 --- a/steps_test.go +++ b/steps_test.go @@ -140,7 +140,7 @@ func TestStepsDrawWithCaption(t *testing.T) { // badgeY = (H - StepBoxH)/2 = (40-16)/2 = 12. captionY := 12 + StepBoxH + StepLabelGap painted := 0 - for y := captionY; y < captionY+GlyphHeight; y++ { + for y := captionY; y < captionY+GlyphHeight(); y++ { for x := 0; x < w; x++ { if pixelAt(buf, w, x, y) == theme.OnBackground { painted++ @@ -163,7 +163,7 @@ func TestStepsDrawEmptyCaptionSkipsBelow(t *testing.T) { buf := makeSurface(w, h) s.Draw(newP(buf, w), theme) captionY := 12 + StepBoxH + StepLabelGap - for y := captionY; y < captionY+GlyphHeight; y++ { + for y := captionY; y < captionY+GlyphHeight(); y++ { for x := 0; x < w; x++ { if pixelAt(buf, w, x, y) == theme.OnBackground { t.Fatalf("empty-caption Steps painted ink at (%d,%d)", x, y) diff --git a/table.go b/table.go index 6b17b52..472601a 100644 --- a/table.go +++ b/table.go @@ -100,7 +100,7 @@ func (t *Table) Draw(p painter.Painter, theme *Theme) { fillRect(p, r.X, r.Y+TableHeaderHeight-1, r.W, 1, theme.Border) // Header cell titles. hx := r.X - hty := r.Y + (TableHeaderHeight-GlyphHeight)/2 + hty := r.Y + (TableHeaderHeight-GlyphHeight())/2 for i, col := range t.Columns { DrawText(p, hx+TableCellPadX, hty, col.Title, theme.OnBackground) hx += widths[i] @@ -113,7 +113,7 @@ func (t *Table) Draw(p painter.Painter, theme *Theme) { // one TableRowHeight below the header. tw := TextWidth(tableEmptyPlaceholder) tx := r.X + (r.W-tw)/2 - ty := bodyY + (TableRowHeight-GlyphHeight)/2 + ty := bodyY + (TableRowHeight-GlyphHeight())/2 DrawText(p, tx, ty, tableEmptyPlaceholder, theme.OnSurface) return } @@ -139,7 +139,7 @@ func (t *Table) Draw(p painter.Painter, theme *Theme) { } fillRect(p, r.X, y, r.W, TableRowHeight, bg) cx := r.X - cty := y + (TableRowHeight-GlyphHeight)/2 + cty := y + (TableRowHeight-GlyphHeight())/2 for j := range t.Columns { if j < len(row) { DrawText(p, cx+TableCellPadX, cty, row[j], ink) diff --git a/textview.go b/textview.go index a9eb06a..1adedcc 100644 --- a/textview.go +++ b/textview.go @@ -82,15 +82,15 @@ func (t *TextView) Draw(p painter.Painter, theme *Theme) { } fillRect(p, r.X, r.Y, r.W, r.H, theme.Surface) strokeRect(p, r.X, r.Y, r.W, r.H, border) - lineH := GlyphHeight + 4 // 1-pixel-line font + 4 px line spacing + lineH := GlyphHeight() + 4 // 1-pixel-line font + 4 px line spacing for i, line := range t.Lines { y := r.Y + 4 + i*lineH DrawText(p, r.X+4, y, line, theme.OnSurface) } if t.Focused { - cx := r.X + 4 + t.CursorCol*GlyphAdvance + cx := r.X + 4 + t.CursorCol*GlyphAdvance() cy := r.Y + 4 + t.CursorLine*lineH - fillRect(p, cx, cy-1, 1, GlyphHeight+2, theme.OnSurface) + fillRect(p, cx, cy-1, 1, GlyphHeight()+2, theme.OnSurface) // IME composition preview: render the pending string in the // muted SurfaceAlt tone starting at the cursor, so the user // sees dead-key / CJK candidates without them entering the @@ -98,7 +98,7 @@ func (t *TextView) Draw(p painter.Painter, theme *Theme) { if t.Composition != "" { cw := TextWidth(t.Composition) DrawText(p, cx, cy, t.Composition, theme.SurfaceAlt) - fillRect(p, cx, cy+GlyphHeight, cw, 1, theme.SurfaceAlt) + fillRect(p, cx, cy+GlyphHeight(), cw, 1, theme.SurfaceAlt) } } } diff --git a/timeline.go b/timeline.go index 7016a77..9842ffd 100644 --- a/timeline.go +++ b/timeline.go @@ -61,10 +61,6 @@ const ( // TimelineMarkerSize is the pixel side of each event's filled // square marker painted on the rail. TimelineMarkerSize = 6 - // TimelineEventH is the vertical stride from one event's Title - // row to the next when the event has NO Detail — one glyph row - // plus 4 px of inter-event spacing. - TimelineEventH = GlyphHeight + 4 // TimelineDetailGap is the vertical space inserted between an // event's Title row and its Detail row when Detail != "". TimelineDetailGap = 2 @@ -77,6 +73,11 @@ const ( TimelinePadY = 8 ) +// TimelineEventH is the vertical stride from one event's Title row to the next +// when the event has NO Detail — one glyph row plus 4 px of inter-event +// spacing. A function, as it derives from the active font's GlyphHeight. +func TimelineEventH() int { return GlyphHeight() + 4 } + // NewTimeline constructs a Timeline carrying the given events. A // nil events slice is normalised to a non-nil empty slice so // downstream code (range loops, len() checks) never has to guard @@ -125,14 +126,14 @@ func (tl *Timeline) Draw(p painter.Painter, theme *Theme) { y := r.Y + TimelinePadY for _, ev := range tl.Events { markerX := railX - TimelineMarkerSize/2 - markerY := y + (GlyphHeight-TimelineMarkerSize)/2 + markerY := y + (GlyphHeight()-TimelineMarkerSize)/2 fillRect(p, markerX, markerY, TimelineMarkerSize, TimelineMarkerSize, timelineMarkerInk(ev.Kind, theme)) DrawText(p, textX, y, ev.Title, theme.OnSurface) - blockH := TimelineEventH + blockH := TimelineEventH() if ev.Detail != "" { - DrawText(p, textX, y+GlyphHeight+TimelineDetailGap, ev.Detail, dimInk(theme)) - blockH += TimelineDetailGap + GlyphHeight + DrawText(p, textX, y+GlyphHeight()+TimelineDetailGap, ev.Detail, dimInk(theme)) + blockH += TimelineDetailGap + GlyphHeight() } y += blockH } diff --git a/timeline_test.go b/timeline_test.go index f1c253f..acf2013 100644 --- a/timeline_test.go +++ b/timeline_test.go @@ -127,7 +127,7 @@ func TestTimelineDrawSingleDefaultEvent(t *testing.T) { // Marker centre pixel is Accent. railX := TimelinePadX + TimelineMarkerW/2 - markerY := TimelinePadY + (GlyphHeight-TimelineMarkerSize)/2 + markerY := TimelinePadY + (GlyphHeight()-TimelineMarkerSize)/2 // Sample the marker's interior (avoid the rail-intersect edge case). if got := pixelAt(buf, w, railX+1, markerY+1); got != theme.Accent { t.Fatalf("marker interior = %+v, want Accent %+v", got, theme.Accent) @@ -135,7 +135,7 @@ func TestTimelineDrawSingleDefaultEvent(t *testing.T) { // Title text lands: scan the title row for OnSurface pixels. textX := TimelinePadX + TimelineMarkerW inked := 0 - for y := TimelinePadY; y < TimelinePadY+GlyphHeight; y++ { + for y := TimelinePadY; y < TimelinePadY+GlyphHeight(); y++ { for x := textX; x < w; x++ { if pixelAt(buf, w, x, y) == theme.OnSurface { inked++ @@ -168,20 +168,20 @@ func TestTimelineDrawEveryKindWithAndWithoutDetail(t *testing.T) { y := TimelinePadY events := tl.Events for i, ev := range events { - markerY := y + (GlyphHeight-TimelineMarkerSize)/2 + markerY := y + (GlyphHeight()-TimelineMarkerSize)/2 wantInk := timelineMarkerInk(ev.Kind, theme) got := pixelAt(buf, w, railX+1, markerY+1) if got != wantInk { t.Fatalf("event %d (%v) marker = %+v, want %+v", i, ev.Kind, got, wantInk) } - blockH := TimelineEventH + blockH := TimelineEventH() if ev.Detail != "" { // Detail row should carry dim ink (dimInk helper) somewhere. - detailY := y + GlyphHeight + TimelineDetailGap + detailY := y + GlyphHeight() + TimelineDetailGap inked := 0 wantDim := dimInk(theme) - for dy := detailY; dy < detailY+GlyphHeight; dy++ { + for dy := detailY; dy < detailY+GlyphHeight(); dy++ { for dx := TimelinePadX + TimelineMarkerW; dx < w; dx++ { if pixelAt(buf, w, dx, dy) == wantDim { inked++ @@ -191,7 +191,7 @@ func TestTimelineDrawEveryKindWithAndWithoutDetail(t *testing.T) { if inked == 0 { t.Fatalf("event %d Detail row painted 0 dim ink pixels", i) } - blockH += TimelineDetailGap + GlyphHeight + blockH += TimelineDetailGap + GlyphHeight() } y += blockH } @@ -207,12 +207,12 @@ func TestTimelineDrawEmptyDetailSkipsSecondRow(t *testing.T) { tl.SetBounds(Rect{X: 0, Y: 0, W: 160, H: 60}) buf := makeSurface(w, h) tl.Draw(newP(buf, w), theme) - detailY := TimelinePadY + GlyphHeight + TimelineDetailGap + detailY := TimelinePadY + GlyphHeight() + TimelineDetailGap textX := TimelinePadX + TimelineMarkerW // Dim ink is used by Detail text. Rail line uses theme.Border so // restrict the scan to the text column past the rail. wantDim := dimInk(theme) - for y := detailY; y < detailY+GlyphHeight; y++ { + for y := detailY; y < detailY+GlyphHeight(); y++ { for x := textX; x < w; x++ { if pixelAt(buf, w, x, y) == wantDim { t.Fatalf("empty Detail painted dim ink at (%d,%d)", x, y) @@ -243,7 +243,7 @@ func TestTimelineDrawDarkTheme(t *testing.T) { t.Fatalf("dark rail = %+v, want dark Border %+v", got, theme.Border) } // Marker = dark Accent. - markerY := TimelinePadY + (GlyphHeight-TimelineMarkerSize)/2 + markerY := TimelinePadY + (GlyphHeight()-TimelineMarkerSize)/2 if got := pixelAt(buf, w, railX+1, markerY+1); got != theme.Accent { t.Fatalf("dark marker = %+v, want dark Accent %+v", got, theme.Accent) } diff --git a/toggle.go b/toggle.go index ff75542..fcbefd9 100644 --- a/toggle.go +++ b/toggle.go @@ -33,7 +33,7 @@ func (t *ToggleButton) Draw(p painter.Painter, theme *Theme) { strokeRect(p, r.X, r.Y, r.W, r.H, theme.Border) tw := TextWidth(t.Label) tx := r.X + (r.W-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, t.Label, theme.OnSurface) } diff --git a/toolbar.go b/toolbar.go index 9acfca8..5e4984e 100644 --- a/toolbar.go +++ b/toolbar.go @@ -85,7 +85,7 @@ func (t *Toolbar) Draw(p painter.Painter, theme *Theme) { } ch := string(label[0]) tx := x + (bw-TextWidth(ch))/2 - ty := r.Y + (bh-GlyphHeight)/2 + ty := r.Y + (bh-GlyphHeight())/2 ink := theme.OnSurface if it.Disabled { ink = theme.Border diff --git a/tooltip.go b/tooltip.go index 5264116..0f2dd8f 100644 --- a/tooltip.go +++ b/tooltip.go @@ -34,7 +34,7 @@ func (t *Tooltip) Show(anchor Rect) { t.Visible = true t.Anchor = anchor w := TextWidth(t.Text) + 2*TooltipPadX - h := GlyphHeight + 2*TooltipPadY + h := GlyphHeight() + 2*TooltipPadY t.SetBounds(Rect{ X: anchor.X, Y: anchor.Y + anchor.H + 2, diff --git a/treeview.go b/treeview.go index 79b7d3e..6d43162 100644 --- a/treeview.go +++ b/treeview.go @@ -113,7 +113,7 @@ func (t *TreeView) Draw(p painter.Painter, theme *Theme) { } } } - textY := y + (rh-GlyphHeight)/2 + textY := y + (rh-GlyphHeight())/2 DrawText(p, indent+TreeChevronW, textY, row.node.Label, ink) } } diff --git a/v04_test.go b/v04_test.go index 6e95e4b..33681ed 100644 --- a/v04_test.go +++ b/v04_test.go @@ -310,7 +310,7 @@ func TestCalendarSelect(t *testing.T) { c.OnSelect = func(y, m, d int) { got = d } c.SetBounds(Rect{X: 0, Y: 0, W: 200, H: 200}) // June 2026 first = Mon -> col 0 row 0. Click the 1st cell. - gridY := CalendarHeaderH + GlyphHeight + 4 + gridY := CalendarHeaderH + GlyphHeight() + 4 c.OnEvent(Event{Kind: EventClick, X: 0, Y: gridY + 2}) if got != 1 { t.Fatalf("want day=1, got %d", got) @@ -352,7 +352,7 @@ func TestCalendarOnEventKeyDown(t *testing.T) { func TestCalendarSelectNegativeCol(t *testing.T) { c := NewCalendar(2026, 6, 1) c.SetBounds(Rect{X: 0, Y: 0, W: 200, H: 200}) - gridY := CalendarHeaderH + GlyphHeight + 4 + gridY := CalendarHeaderH + GlyphHeight() + 4 c.OnEvent(Event{Kind: EventClick, X: -50, Y: gridY + 2}) // col<0 branch } diff --git a/viewswitcher.go b/viewswitcher.go index eaa58e9..f04979e 100644 --- a/viewswitcher.go +++ b/viewswitcher.go @@ -80,7 +80,7 @@ func (v *ViewSwitcher) Draw(p painter.Painter, theme *Theme) { } tw := TextWidth(title) tx := sx + (segW-tw)/2 - ty := r.Y + (r.H-GlyphHeight)/2 + ty := r.Y + (r.H-GlyphHeight())/2 DrawText(p, tx, ty, title, ink) } } diff --git a/widget_test.go b/widget_test.go index 7531bef..b1fd9a2 100644 --- a/widget_test.go +++ b/widget_test.go @@ -326,17 +326,17 @@ func TestLabelDrawPaintsBitmapText(t *testing.T) { } } -// When Bounds.H <= GlyphHeight the label paints at Bounds.Y (the +// When Bounds.H <= GlyphHeight() the label paints at Bounds.Y (the // centring branch is skipped). Cover that branch separately. func TestLabelDrawTightBoundsSkipsCentring(t *testing.T) { const w, h = 32, 12 theme := DefaultLight() l := NewLabel("A") - l.SetBounds(Rect{X: 2, Y: 2, W: 20, H: GlyphHeight}) + l.SetBounds(Rect{X: 2, Y: 2, W: 20, H: GlyphHeight()}) buf := makeSurface(w, h) l.Draw(newP(buf, w), theme) // 'A' column 0 has bits[0]=0x7E (rows 1..6 lit). At tight Bounds - // with H == GlyphHeight, ty stays at Bounds.Y = 2. Row 1 relative + // with H == GlyphHeight(), ty stays at Bounds.Y = 2. Row 1 relative // to Bounds.Y is (2 + 1) = 3. if pixelAt(buf, w, 2, 3) != theme.OnSurface { t.Fatalf("expected ink at (2,3) with tight bounds; got %+v", pixelAt(buf, w, 2, 3))