Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Public `Graphik` methods:
- `getVersion()` — returns the installed graphik version string.
- `drawRectangle(xpos, ypos, width, height, color)` — `xpos`/`ypos` are the rectangle's top-left corner.
- `drawText(text, xpos, ypos, size, color)` — `xpos`/`ypos` are the **center** of the rendered text, not its top-left corner. This differs from `drawRectangle`, `drawButton` and `drawImage`, which all position their top-left corner at the given coordinates.
- `drawButton(xpos, ypos, width, height, colorBox, colorText, sizeText, text, function)` — draws a rectangle and centered text, and calls `function()` on every call where the mouse is held inside the button with button 1 down (there is no click-edge detection, so a held-down mouse fires `function()` once per call, not once per click). Callers wanting once-per-click semantics must debounce on their side.
- `drawButton(xpos, ypos, width, height, colorBox, colorText, sizeText, text, function)` — draws a rectangle and centered text, and calls `function()` on every call where the mouse is held inside the button with button 1 down (there is no click-edge detection, so a held-down mouse fires `function()` once per call, not once per click). Callers wanting once-per-click semantics must debounce on their side. The clickable region is exactly the drawn box — `xpos` through `xpos + width - 1`, and `ypos` through `ypos + height - 1` — so buttons laid out edge to edge share no clickable coordinate.
- `drawImage(filePath, xpos, ypos, width, height)` — caches the loaded and scaled surface by `filePath`, so an image edited on disk mid-run will not be picked up until the process restarts.

Color constants: `Graphik.black`, `Graphik.white`, `Graphik.red`, `Graphik.green`, `Graphik.blue`.
Expand Down
13 changes: 12 additions & 1 deletion src/main/python/preponderous/graphik/graphik.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ def drawButton(self, xpos, ypos, width, height, colorBox, colorText, sizeText, t
click. A caller wanting once-per-click semantics must debounce on its
own side; see the implementation note below for why.

The clickable region is exactly the region the box is drawn over:
``xpos`` through ``xpos + width - 1`` horizontally, and ``ypos``
through ``ypos + height - 1`` vertically. Buttons laid out edge to
edge therefore share no clickable coordinate -- a boundary belongs to
the button whose left/top edge sits on it.

Args:
xpos: X coordinate of the box's left edge, in pixels.
ypos: Y coordinate of the box's top edge, in pixels.
Expand All @@ -168,7 +174,12 @@ def drawButton(self, xpos, ypos, width, height, colorBox, colorText, sizeText, t

# if clicked then do function
mouse = pygame.mouse.get_pos()
if (xpos + width > mouse[0] > xpos and ypos + height > mouse[1] > ypos):
# Half-open on both axes, matching the range pygame.draw.rect fills, so
# the clickable region is exactly the drawn one. A strict `> xpos` here
# would leave the painted left and top edge columns unclickable, and a
# closed `<= xpos + width` would let edge-to-edge buttons both claim
# their shared boundary.
if (xpos <= mouse[0] < xpos + width and ypos <= mouse[1] < ypos + height):
click = pygame.mouse.get_pressed()
if click[0] == 1:
function()
Expand Down
66 changes: 66 additions & 0 deletions src/test/python/preponderous/graphik/test_graphik.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,72 @@ def test_draw_button_invokes_callback_only_on_inside_click(
assert calls == ([True] if expect_call else [])


def _click_at(monkeypatch, graphik, pos, box=(10, 10, 20, 20)):
# Draw one button and report whether a press at `pos` reached the callback.
calls = []
monkeypatch.setattr(pygame.mouse, "get_pos", lambda: pos)
monkeypatch.setattr(pygame.mouse, "get_pressed", lambda: (1, 0, 0))
xpos, ypos, width, height = box
graphik.drawButton(
xpos, ypos, width, height, Graphik.blue, Graphik.white, 10, "Go", lambda: calls.append(True)
)
return bool(calls)


@pytest.mark.parametrize(
"pos, expect_call",
[
# The box drawn at (10,10) 20x20 covers pixels 10..29 on both axes, so
# every one of its four edges must be clickable...
pytest.param((10, 15), True, id="left_edge"),
pytest.param((15, 10), True, id="top_edge"),
pytest.param((29, 15), True, id="right_edge"),
pytest.param((15, 29), True, id="bottom_edge"),
pytest.param((10, 10), True, id="top_left_corner"),
# ...and nothing outside that region may be, including the first
# coordinate past the far edge, which is not painted.
pytest.param((9, 15), False, id="just_left_of_box"),
pytest.param((15, 9), False, id="just_above_box"),
pytest.param((30, 15), False, id="just_right_of_box"),
pytest.param((15, 30), False, id="just_below_box"),
],
)
def test_draw_button_clickable_region_matches_the_drawn_box(monkeypatch, pos, expect_call):
# Regression guard: the hit test used strict inequalities on both axes, which
# left the painted left and top edge lines dead while the right and bottom
# ones worked. The clickable region must be exactly the drawn region.
graphik = _make_graphik((40, 40))
assert _click_at(monkeypatch, graphik, pos) == expect_call


def test_draw_button_edges_are_painted_where_they_are_clickable():
# Anchors the test above to what is actually drawn, so the two cannot drift:
# the edge pixels asserted clickable are the same ones filled with colorBox.
graphik = _make_graphik((40, 40))
display = graphik.getGameDisplay()
display.fill(Graphik.black)

graphik.drawButton(10, 10, 20, 20, Graphik.blue, Graphik.white, 10, "Go", lambda: None)

assert _rgb(display, (10, 15)) == Graphik.blue
assert _rgb(display, (15, 10)) == Graphik.blue
assert _rgb(display, (29, 15)) == Graphik.blue
assert _rgb(display, (15, 29)) == Graphik.blue
# One past the far edge is outside the fill, matching the half-open bounds.
assert _rgb(display, (30, 15)) == Graphik.black
assert _rgb(display, (15, 30)) == Graphik.black


def test_adjacent_buttons_do_not_share_a_clickable_boundary(monkeypatch):
# With half-open bounds a shared boundary belongs to exactly one button, so
# stacking buttons edge to edge cannot fire both callbacks from one press.
graphik = _make_graphik((60, 40))
left = _click_at(monkeypatch, graphik, (30, 15), box=(10, 10, 20, 20))
right = _click_at(monkeypatch, graphik, (30, 15), box=(30, 10, 20, 20))

assert (left, right) == (False, True)


def test_draw_button_fires_callback_once_per_call_while_mouse_held(monkeypatch):
# Pins the documented repeat-fire behavior: there is no click-edge
# detection, so a held-down mouse inside the button fires the callback
Expand Down
Loading