Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions buildconfig/stubs/pygame/font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class Font:
def point_size(self) -> int: ...
@point_size.setter
def point_size(self, value: int) -> None: ...
@property
def outline(self) -> int: ...
@outline.setter
def outline(self, value: int) -> None: ...
def __init__(self, filename: Optional[FileLike] = None, size: int = 20) -> None: ...
def render(
self,
Expand Down Expand Up @@ -87,6 +91,8 @@ class Font:
def set_direction(self, direction: int) -> None: ...
def get_point_size(self) -> int: ...
def set_point_size(self, val: int, /) -> None: ...
def get_outline(self) -> int: ...
def set_outline(self, val: int, /) -> None: ...

@deprecated("Use `Font` instead (FontType is an old alias)")
class FontType(Font): ...
42 changes: 42 additions & 0 deletions docs/reST/ref/font.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,22 @@ solves no longer exists, it will likely be removed in the future.

.. ## Font.point_size ##


.. attribute:: outline

| :sl:`Gets or sets the font's outline value`
| :sg:`outline -> int`

The outline value of the font.

When set to 0, the font will be drawn normally. When positive,
the text will be drawn as a hollow outline. This can be drawn
underneath unoutlined text to create a text outline effect.

.. versionadded:: 2.5.6

.. ## Font.outline ##

.. method:: render

| :sl:`draw text on a new Surface`
Expand Down Expand Up @@ -562,6 +578,32 @@ solves no longer exists, it will likely be removed in the future.

.. ## Font.get_point_size ##

.. method:: set_outline

| :sl:`set the outline value of the font`
| :sg:`set_outline(size, /) -> None`

Sets the outline value of the font.

.. note:: This is the same as the :attr:`outline` attribute.

.. versionadded:: 2.5.6

.. ## Font.set_outline ##

.. method:: get_outline

| :sl:`get the outline value of the font`
| :sg:`get_outline() -> int`

Returns the outline value of the font.

.. note:: This is the same as the :attr:`outline` attribute.

.. versionadded:: 2.5.6

.. ## Font.get_outline ##

.. method:: get_ascent

| :sl:`get the ascent of the font`
Expand Down
3 changes: 3 additions & 0 deletions src_c/doc/font_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define DOC_FONT_FONT_STRIKETHROUGH "strikethrough -> bool\nGets or sets whether the font should be rendered with a strikethrough."
#define DOC_FONT_FONT_ALIGN "align -> int\nSet how rendered text is aligned when given a wrap length."
#define DOC_FONT_FONT_POINTSIZE "point_size -> int\nGets or sets the font's point size"
#define DOC_FONT_FONT_OUTLINE "outline -> int\nGets or sets the font's outline value"
#define DOC_FONT_FONT_RENDER "render(text, antialias, color, bgcolor=None, wraplength=0) -> Surface\ndraw text on a new Surface"
#define DOC_FONT_FONT_SIZE "size(text, /) -> (width, height)\ndetermine the amount of space needed to render text"
#define DOC_FONT_FONT_SETUNDERLINE "set_underline(bool, /) -> None\ncontrol if text is rendered with an underline"
Expand All @@ -33,6 +34,8 @@
#define DOC_FONT_FONT_GETHEIGHT "get_height() -> int\nget the height of the font"
#define DOC_FONT_FONT_SETPOINTSIZE "set_point_size(size, /) -> None\nset the point size of the font"
#define DOC_FONT_FONT_GETPOINTSIZE "get_point_size() -> int\nget the point size of the font"
#define DOC_FONT_FONT_SETOUTLINE "set_outline(outline, /) -> None\nset The outline value of the font"
#define DOC_FONT_FONT_GETOUTLINE "get_outline() -> int\nget the outline value of the font"
#define DOC_FONT_FONT_GETASCENT "get_ascent() -> int\nget the ascent of the font"
#define DOC_FONT_FONT_GETDESCENT "get_descent() -> int\nget the descent of the font"
#define DOC_FONT_FONT_SETSCRIPT "set_script(str, /) -> None\nset the script code for text shaping"
Expand Down
94 changes: 94 additions & 0 deletions src_c/font.c
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,96 @@ font_set_ptsize(PyObject *self, PyObject *arg)
#endif
}

static PyObject *
font_getter_outline(PyObject *self, void *closure)
{
if (!PgFont_GenerationCheck(self)) {
return RAISE_FONT_QUIT_ERROR();
}

#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
TTF_Font *font = PyFont_AsFont(self);
return PyLong_FromLong(TTF_GetFontOutline(font));
#else
return RAISE(pgExc_SDLError,
"pygame.font not compiled with a new enough SDL_ttf version. "
"Needs SDL_ttf 2.0.12 or above.");
#endif
}

static int
font_setter_outline(PyObject *self, PyObject *value, void *closure)
{
if (!PgFont_GenerationCheck(self)) {
RAISE_FONT_QUIT_ERROR_RETURN(-1);
}
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
TTF_Font *font = PyFont_AsFont(self);

DEL_ATTR_NOT_SUPPORTED_CHECK("outline", value);

if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "outline must be an integer");
return -1;
}
long val = PyLong_AsLong(value);
if (val == -1 && PyErr_Occurred()) {
return -1;
}
if (val < 0) {
PyErr_SetString(PyExc_ValueError, "outline must be >= 0");
return -1;
}
TTF_SetFontOutline(font, (int)val);
return 0;
#else
RAISE(pgExc_SDLError,
"pygame.font not compiled with a new enough SDL_ttf version. Needs SDL_ttf 2.0.12 or above.");
return -1;
#endif
}

static PyObject *
font_get_outline(PyObject *self, PyObject *_null)
{
if (!PgFont_GenerationCheck(self)) {
return RAISE_FONT_QUIT_ERROR();
}
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
TTF_Font *font = PyFont_AsFont(self);
return PyLong_FromLong(TTF_GetFontOutline(font));
#else
return RAISE(pgExc_SDLError,
"pygame.font not compiled with a new enough SDL_ttf version. "
"Needs SDL_ttf 2.0.12 or above.");
#endif
}

static PyObject *
font_set_outline(PyObject *self, PyObject *arg)
{
if (!PgFont_GenerationCheck(self)) {
return RAISE_FONT_QUIT_ERROR();
}
#if SDL_TTF_VERSION_ATLEAST(2, 0, 12)
TTF_Font *font = PyFont_AsFont(self);
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return NULL;
}
if (val < 0) {
return RAISE(PyExc_ValueError, "outline must be >= 0");
}
TTF_SetFontOutline(font, (int)val);
Py_RETURN_NONE;
#else
return RAISE(pgExc_SDLError,
"pygame.font not compiled with a new enough SDL_ttf version. "
"Needs SDL_ttf 2.0.12 or above.");
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got code duplication. I suggest moving the common code into a helper function that gets called by both the dedicated setter and the property setter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur; I considered deduplicating the functions, but since the rest of the file seemed to use this sort of duplication I erred toward consistency. Is there a convention for this kind of deduplication elsewhere in the project?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's scattered from what I remember. Personally, I'm a huge advocate for not duplicating code when possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote font_get_outline to just call font_getter_outline (We could probably even tell python to call font_getter_outline instead with a type cast?), and font_set_outline to call font_setter_outline and massage the return value.

Common helper functions would work too, but I don't see a reason to define 6 functions when 4 (3?) will give the same result.

}


static PyObject *
font_getter_name(PyObject *self, void *closure)
{
Expand Down Expand Up @@ -1168,6 +1258,8 @@ static PyGetSetDef font_getsets[] = {
DOC_FONT_FONT_UNDERLINE, NULL},
{"strikethrough", (getter)font_getter_strikethrough,
(setter)font_setter_strikethrough, DOC_FONT_FONT_STRIKETHROUGH, NULL},
{"outline", (getter)font_getter_outline, (setter)font_setter_outline,
DOC_FONT_FONT_OUTLINE, NULL},
{"align", (getter)font_getter_align, (setter)font_setter_align,
DOC_FONT_FONT_ALIGN, NULL},
{"point_size", (getter)font_getter_point_size,
Expand All @@ -1192,6 +1284,8 @@ static PyMethodDef font_methods[] = {
DOC_FONT_FONT_GETSTRIKETHROUGH},
{"set_strikethrough", font_set_strikethrough, METH_O,
DOC_FONT_FONT_SETSTRIKETHROUGH},
{"get_outline", font_get_outline, METH_NOARGS, DOC_FONT_FONT_GETOUTLINE},
{"set_outline", font_set_outline, METH_O, DOC_FONT_FONT_SETOUTLINE},
{"get_point_size", font_get_ptsize, METH_NOARGS,
DOC_FONT_FONT_GETPOINTSIZE},
{"set_point_size", font_set_ptsize, METH_O, DOC_FONT_FONT_SETPOINTSIZE},
Expand Down
87 changes: 86 additions & 1 deletion test/font_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,70 @@ def test_point_size_method(self):
self.assertRaises(ValueError, f.set_point_size, -500)
self.assertRaises(TypeError, f.set_point_size, "15")

def test_outline_property(self):
if pygame_font.__name__ == "pygame.ftfont":
return # not a pygame.ftfont feature

pygame_font.init()
font_path = os.path.join(
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
)
f = pygame_font.Font(pathlib.Path(font_path), 25)

ttf_version = pygame_font.get_sdl_ttf_version()
if ttf_version < (2, 0, 12):
with self.assertRaises(pygame.error):
f.outline = 0
with self.assertRaises(pygame.error):
_ = f.outline
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cleaner to just skip this test in this case using unittest.skipIf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be cleaner, but that would also mean the case of trying to use the property on an unsupported version is not being tested. Is that a concern?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have a dedicated test for it that skips if the SDL_ttf version is too new. I'm not a huge fan of having this weird logic in the middle of the test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use skipIf, and added tests for the stubs with 935ab5c.


# Default outline should be an integer >= 0 (typically 0)
self.assertIsInstance(f.outline, int)
self.assertGreaterEqual(f.outline, 0)

orig = f.outline
f.outline = orig + 1
self.assertEqual(orig + 1, f.outline)
f.outline += 2
self.assertEqual(orig + 3, f.outline)
f.outline -= 1
self.assertEqual(orig + 2, f.outline)

def test_neg():
f.outline = -1

def test_incorrect_type():
f.outline = "2"

self.assertRaises(ValueError, test_neg)
self.assertRaises(TypeError, test_incorrect_type)

def test_outline_method(self):
if pygame_font.__name__ == "pygame.ftfont":
return # not a pygame.ftfont feature

pygame_font.init()
font_path = os.path.join(
os.path.split(pygame.__file__)[0], pygame_font.get_default_font()
)
f = pygame_font.Font(pathlib.Path(font_path), 25)

ttf_version = pygame_font.get_sdl_ttf_version()
if ttf_version < (2, 0, 12):
self.assertRaises(pygame.error, f.get_outline)
self.assertRaises(pygame.error, f.set_outline, 1)
return

val0 = f.get_outline()
self.assertIsInstance(val0, int)
self.assertGreaterEqual(val0, 0)

f.set_outline(5)
self.assertEqual(5, f.get_outline())
self.assertRaises(ValueError, f.set_outline, -1)
self.assertRaises(TypeError, f.set_outline, "2")

def test_font_name(self):
f = pygame_font.Font(None, 20)
self.assertEqual(f.name, "FreeSans")
Expand Down Expand Up @@ -933,6 +997,14 @@ def test_font_method_should_raise_exception_after_quit(self):
]
skip_methods = set()
version = pygame.font.get_sdl_ttf_version()

if version >= (2, 0, 12):
methods.append(("get_outline", ()))
methods.append(("set_outline", (2,)))
else:
skip_methods.add("get_outline")
skip_methods.add("set_outline")

if version >= (2, 0, 18):
methods.append(("get_point_size", ()))
methods.append(("set_point_size", (34,)))
Expand Down Expand Up @@ -1032,6 +1104,11 @@ def test_font_property_should_raise_exception_after_quit(self):
else:
skip_properties.add("point_size")

if version >= (2, 0, 12):
properties.append(("outline", 1))
else:
skip_properties.add("outline")

font = pygame_font.Font(None, 10)
actual_names = []

Expand Down Expand Up @@ -1096,6 +1173,7 @@ def query(
underline=False,
strikethrough=False,
antialiase=False,
outline=0
):
if self.aborted:
return False
Expand All @@ -1106,7 +1184,7 @@ def query(
screen = self.screen
screen.fill((255, 255, 255))
pygame.display.flip()
if not (bold or italic or underline or strikethrough or antialiase):
if not (bold or italic or underline or strikethrough or antialiase or outline):
text = "normal"
else:
modes = []
Expand All @@ -1120,18 +1198,22 @@ def query(
modes.append("strikethrough")
if antialiase:
modes.append("antialiased")
if outline:
modes.append("outlined")
text = f"{'-'.join(modes)} (y/n):"
f.set_bold(bold)
f.set_italic(italic)
f.set_underline(underline)
f.set_strikethrough(strikethrough)
f.set_outline(outline)
s = f.render(text, antialiase, (0, 0, 0))
screen.blit(s, (offset, y))
y += s.get_size()[1] + spacing
f.set_bold(False)
f.set_italic(False)
f.set_underline(False)
f.set_strikethrough(False)
f.set_outline(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it needs to guard against SDL_ttf version as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added guards with 935ab5c.

s = f.render("(some comparison text)", False, (0, 0, 0))
screen.blit(s, (offset, y))
pygame.display.flip()
Expand Down Expand Up @@ -1173,6 +1255,9 @@ def test_italic_underline(self):
def test_bold_strikethrough(self):
self.assertTrue(self.query(bold=True, strikethrough=True))

def test_outline(self):
self.assertTrue(self.query(outline=1))


if __name__ == "__main__":
unittest.main()
Loading