Both renderers placed the baseline at box_top + height/2 + descent/2. Centring glyphs of visual height ascent+descent in a box of height H puts the baseline at box_top + H/2 + (ascent-descent)/2; the two agree only when ascent is exactly twice descent. DejaVu is nearer 4:1, so labels sat high against the top edge - measured at 5px above and 11px below for a 14px button. ButtonText also sized itself from the nominal font size, which is smaller than the text's visual height (17px of ink for a 14px DejaVu font), leaving the button too short to centre its label in. It now measures ascent+descent, with a fallback for font objects that cannot report metrics. docs/images/example_07_pressed_state.png was stale - no example writes it, the demo emits demo_07_pressed.png at the repo root and the docs copy had been placed by hand in November. Refreshed here; the demo should write straight to docs/images/ so it cannot drift again.
125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""
|
|
Regression tests for vertical centring of text in buttons and form fields.
|
|
|
|
Both placed the baseline at `top + height/2 + descent/2`. Centring text whose
|
|
visual height is ascent+descent inside a box of height H puts the baseline at
|
|
`top + H/2 + (ascent-descent)/2`; the two agree only when ascent == 2*descent.
|
|
Real fonts have a much larger ratio - DejaVu is nearer 4:1 - so the text sat
|
|
several pixels high, hugging the top edge of the button.
|
|
|
|
The button was also sized from the nominal font size rather than the text's
|
|
actual visual height, leaving it too short to centre anything in.
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from PIL import Image, ImageDraw
|
|
|
|
from pyWebLayout.abstract.functional import Button, FormField, FormFieldType
|
|
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
|
|
from pyWebLayout.style import Font
|
|
|
|
|
|
CANVAS = (300, 120)
|
|
PADDING = (6, 10, 6, 10) # top, right, bottom, left
|
|
|
|
|
|
@pytest.fixture
|
|
def draw_ctx():
|
|
image = Image.new("RGB", CANVAS, (255, 255, 255))
|
|
return image, ImageDraw.Draw(image)
|
|
|
|
|
|
def ink_rows(image, box):
|
|
"""
|
|
Rows within box that carry text ink.
|
|
|
|
Only the central columns are sampled: the button has rounded corners, so the
|
|
page background shows through at the extremes of every row and would read as
|
|
white text on all of them.
|
|
"""
|
|
x0, y0, x1, y1 = box
|
|
inset = (x1 - x0) // 4
|
|
pixels = image.convert("RGB").load()
|
|
rows = []
|
|
for y in range(y0, y1):
|
|
for x in range(x0 + inset, x1 - inset):
|
|
r, g, b = pixels[x, y]
|
|
# Button text is white on a blue fill; look for near-white ink.
|
|
if r > 240 and g > 240 and b > 240:
|
|
rows.append(y)
|
|
break
|
|
return rows
|
|
|
|
|
|
class TestButtonTextCentring:
|
|
|
|
@pytest.mark.parametrize("font_size", [10, 14, 20])
|
|
def test_text_is_vertically_centred(self, draw_ctx, font_size):
|
|
image, draw = draw_ctx
|
|
font = Font(font_size=font_size, colour=(255, 255, 255))
|
|
button = ButtonText(Button(label="Save Document", callback=lambda p: None),
|
|
font, draw, padding=PADDING)
|
|
button.set_origin(np.array([20, 20]))
|
|
button.render()
|
|
|
|
x0, y0 = 20, 20
|
|
x1 = x0 + int(button.size[0])
|
|
y1 = y0 + int(button.size[1])
|
|
rows = ink_rows(image, (x0, y0, x1, y1))
|
|
assert rows, "the button should have visible text"
|
|
|
|
gap_above = min(rows) - y0
|
|
gap_below = y1 - max(rows) - 1
|
|
|
|
assert abs(gap_above - gap_below) <= 2, (
|
|
f"text not centred at size {font_size}: "
|
|
f"{gap_above}px above, {gap_below}px below")
|
|
|
|
def test_button_is_tall_enough_for_its_text(self):
|
|
font = Font(font_size=14, colour=(255, 255, 255))
|
|
image = Image.new("RGB", CANVAS, (255, 255, 255))
|
|
draw = ImageDraw.Draw(image)
|
|
button = ButtonText(Button(label="Cancel", callback=lambda p: None),
|
|
font, draw, padding=PADDING)
|
|
|
|
ascent, descent = font.font.getmetrics()
|
|
assert int(button.size[1]) >= ascent + descent + PADDING[0] + PADDING[2], \
|
|
"button height must accommodate the text's visual height, not the nominal size"
|
|
|
|
def test_text_stays_inside_the_button(self, draw_ctx):
|
|
image, draw = draw_ctx
|
|
font = Font(font_size=14, colour=(255, 255, 255))
|
|
button = ButtonText(Button(label="Save Document", callback=lambda p: None),
|
|
font, draw, padding=PADDING)
|
|
button.set_origin(np.array([20, 20]))
|
|
button.render()
|
|
|
|
y0, y1 = 20, 20 + int(button.size[1])
|
|
rows = ink_rows(image, (20, y0, 20 + int(button.size[0]), y1))
|
|
assert min(rows) >= y0, "text escaped above the button"
|
|
assert max(rows) < y1, "text escaped below the button"
|
|
|
|
|
|
class TestFormFieldValueCentring:
|
|
|
|
def test_value_is_centred_in_the_input_box(self):
|
|
image = Image.new("RGB", (300, 120), (0, 0, 0))
|
|
draw = ImageDraw.Draw(image)
|
|
font = Font(font_size=12, colour=(0, 0, 0))
|
|
field = FormField(name="who", field_type=FormFieldType.TEXT, value="Hello")
|
|
renderable = FormFieldText(field, font, draw, field_height=28)
|
|
renderable.set_origin(np.array([10, 10]))
|
|
renderable.render()
|
|
|
|
field_y = 10 + font.font_size + 5
|
|
pixels = image.convert("RGB").load()
|
|
rows = [y for y in range(field_y, field_y + 28)
|
|
if any(pixels[x, y] == (0, 0, 0) for x in range(12, 200))]
|
|
assert rows, "the field value should be visible"
|
|
|
|
gap_above = min(rows) - field_y
|
|
gap_below = (field_y + 28) - max(rows) - 1
|
|
assert abs(gap_above - gap_below) <= 3, (
|
|
f"field value not centred: {gap_above}px above, {gap_below}px below")
|