fix(functional): form field labels no longer overprint the field above (S15)
FormFieldText treats its origin as the control's top-left - size and in_object both measure down from it - but drew the label by calling Text.render at that origin, and Text anchors on the baseline. The label's glyphs therefore landed above the origin, outside the box the control claims, on top of whatever was there. In a stacked form that is the preceding field's input box, which is what example_10_forms.png showed: every label but the first crowding the box above it. The label is now offset down by its ascent, so it occupies the space the control accounts for. Height derives from the label's ink height rather than the nominal font size, which had also eaten into the 5px gap between label and box. LABEL_GAP names that gap and field_area_offset gives the distance from the origin to the top of the input box; render, handle_click and the height calculation now share it instead of each recomputing font_size + 5. Also recorded under S12: the broken process pool is not merely wasted work. It forks from a process that already has threads, and tests/layout/test_ereader_image_rendering.py hangs at interpreter exit roughly one run in four - every test passes, then the process never returns.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Regression tests for form field label geometry (spec S15).
|
||||
|
||||
Text renders with a baseline anchor, so drawing the label at the field's origin
|
||||
put its glyphs above that origin - outside the box the field claims through size
|
||||
and in_object. Stacked fields therefore had each label overprinting the input box
|
||||
of the field before it.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from pyWebLayout.abstract.functional import Form, FormField, FormFieldType
|
||||
from pyWebLayout.concrete.functional import FormFieldText
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.layout.document_layouter import form_layouter
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
|
||||
ORIGIN = (10, 40)
|
||||
FIELD_HEIGHT = 24
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def font():
|
||||
return Font(font_size=12, colour=(0, 0, 0))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def canvas():
|
||||
image = Image.new("RGB", (300, 200), (255, 255, 255))
|
||||
return image, ImageDraw.Draw(image)
|
||||
|
||||
|
||||
def make_field(font, draw, label="Email Address"):
|
||||
field = FormField(name="email", field_type=FormFieldType.TEXT, label=label)
|
||||
renderable = FormFieldText(field, font, draw, field_height=FIELD_HEIGHT)
|
||||
renderable.set_origin(np.array(list(ORIGIN)))
|
||||
return renderable
|
||||
|
||||
|
||||
def ink_rows(image, x_range, y_range):
|
||||
pixels = image.convert("RGB").load()
|
||||
return [y for y in y_range
|
||||
if any(sum(pixels[x, y]) < 400 for x in x_range)]
|
||||
|
||||
|
||||
class TestLabelStaysInsideTheFieldBox:
|
||||
|
||||
def test_label_ink_is_below_the_origin(self, font, canvas):
|
||||
image, draw = canvas
|
||||
renderable = make_field(font, draw)
|
||||
renderable.render()
|
||||
|
||||
rows = ink_rows(image, range(ORIGIN[0], ORIGIN[0] + 140),
|
||||
range(0, ORIGIN[1]))
|
||||
assert not rows, \
|
||||
f"label drew above its own origin, at rows {rows}"
|
||||
|
||||
def test_label_and_box_do_not_overlap(self, font, canvas):
|
||||
image, draw = canvas
|
||||
renderable = make_field(font, draw)
|
||||
renderable.render()
|
||||
|
||||
ascent, descent = font.font.getmetrics()
|
||||
label_bottom = ORIGIN[1] + ascent + descent
|
||||
box_top = renderable.field_area_offset + ORIGIN[1]
|
||||
|
||||
assert box_top >= label_bottom, \
|
||||
"the input box must start below the label's descenders"
|
||||
|
||||
def test_reported_height_covers_everything_drawn(self, font, canvas):
|
||||
image, draw = canvas
|
||||
renderable = make_field(font, draw)
|
||||
renderable.render()
|
||||
|
||||
top, bottom = ORIGIN[1], ORIGIN[1] + int(renderable.size[1])
|
||||
rows = ink_rows(image, range(ORIGIN[0], ORIGIN[0] + 200), range(0, 200))
|
||||
assert min(rows) >= top, "ink above the field's declared box"
|
||||
assert max(rows) < bottom, "ink below the field's declared box"
|
||||
|
||||
|
||||
class TestStackedFieldsDoNotCollide:
|
||||
|
||||
def test_form_layout_leaves_labels_clear(self, font):
|
||||
page = Page(size=(300, 400), style=PageStyle())
|
||||
form = Form("signup")
|
||||
for name in ["Username", "Email Address", "Password"]:
|
||||
form.add_field(FormField(name=name.lower().replace(" ", "_"),
|
||||
field_type=FormFieldType.TEXT, label=name))
|
||||
|
||||
ok, ids = form_layouter(form, page, font)
|
||||
assert ok and len(ids) == 3
|
||||
|
||||
fields = [c for c in page.children if isinstance(c, FormFieldText)]
|
||||
assert len(fields) == 3
|
||||
|
||||
for earlier, later in zip(fields, fields[1:]):
|
||||
earlier_bottom = earlier.origin[1] + earlier.size[1]
|
||||
assert later.origin[1] >= earlier_bottom, \
|
||||
"fields overlap: a label would print over the preceding input box"
|
||||
|
||||
def test_rendered_form_has_no_ink_collisions(self, font):
|
||||
"""Every field's ink stays within its own declared bounds."""
|
||||
page = Page(size=(300, 400), style=PageStyle())
|
||||
form = Form("signup")
|
||||
for name in ["Username", "Email Address"]:
|
||||
form.add_field(FormField(name=name.lower(), field_type=FormFieldType.TEXT,
|
||||
label=name))
|
||||
form_layouter(form, page, font)
|
||||
image = page.render()
|
||||
|
||||
fields = [c for c in page.children if isinstance(c, FormFieldText)]
|
||||
for field in fields:
|
||||
top = int(field.origin[1])
|
||||
bottom = top + int(field.size[1])
|
||||
rows = ink_rows(image, range(int(field.origin[0]),
|
||||
int(field.origin[0] + field.size[0])),
|
||||
range(max(0, top - 6), top))
|
||||
assert not rows, f"ink found just above a field at y={top}"
|
||||
|
||||
|
||||
class TestClickTargetsFollowTheLayout:
|
||||
|
||||
def test_click_in_the_input_area_focuses(self, font, canvas):
|
||||
_, draw = canvas
|
||||
renderable = make_field(font, draw)
|
||||
|
||||
inside = (5, renderable.field_area_offset + FIELD_HEIGHT // 2)
|
||||
assert renderable.handle_click(inside) is True
|
||||
assert renderable._focused is True
|
||||
|
||||
def test_click_on_the_label_does_not_focus(self, font, canvas):
|
||||
_, draw = canvas
|
||||
renderable = make_field(font, draw)
|
||||
|
||||
on_label = (5, 2)
|
||||
assert renderable.handle_click(on_label) is False
|
||||
Reference in New Issue
Block a user