diff --git a/buildconfig/stubs/pygame/font.pyi b/buildconfig/stubs/pygame/font.pyi index 99587d9b76..9138729b02 100644 --- a/buildconfig/stubs/pygame/font.pyi +++ b/buildconfig/stubs/pygame/font.pyi @@ -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, diff --git a/docs/reST/ref/font.rst b/docs/reST/ref/font.rst index 8057bb46f2..7f830e73bd 100644 --- a/docs/reST/ref/font.rst +++ b/docs/reST/ref/font.rst @@ -306,6 +306,44 @@ 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 thickness (pixels)` + | :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. The outline grows in all + directions a number of pixels equal to the value set. Negative values + are not allowed. + + This can be drawn underneath unoutlined text to create a text outline + effect. For example: :: + + def render_outlined( + font: pygame.Font, + text: str, + text_color: pygame.typing.ColorLike, + outline_color: pygame.typing.ColorLike, + outline_width: int, + ) -> pygame.Surface: + old_outline = font.outline + if old_outline != 0: + font.outline = 0 + base_text_surf = font.render(text, True, text_color) + font.outline = outline_width + outlined_text_surf = font.render(text, True, outline_color) + + outlined_text_surf.blit(base_text_surf, (outline_width, outline_width)) + font.outline = old_outline + return outlined_text_surf + + .. versionadded:: 2.5.6 + + .. ## Font.outline ## + .. method:: render | :sl:`draw text on a new Surface` diff --git a/src_c/doc/font_doc.h b/src_c/doc/font_doc.h index 486e2d9088..b100ce3a65 100644 --- a/src_c/doc/font_doc.h +++ b/src_c/doc/font_doc.h @@ -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 thickness (pixels)" #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" diff --git a/src_c/font.c b/src_c/font.c index 11b7b20530..c28cb0d94a 100644 --- a/src_c/font.c +++ b/src_c/font.c @@ -901,6 +901,47 @@ 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(); + } + + TTF_Font *font = PyFont_AsFont(self); + return PyLong_FromLong(TTF_GetFontOutline(font)); +} + +static int +font_setter_outline(PyObject *self, PyObject *value, void *closure) +{ + if (!PgFont_GenerationCheck(self)) { + RAISE_FONT_QUIT_ERROR_RETURN(-1); + } + TTF_Font *font = PyFont_AsFont(self); + + DEL_ATTR_NOT_SUPPORTED_CHECK("outline", value); + + 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; + } + +#if SDL_TTF_VERSION_ATLEAST(3, 0, 0) + if (!TTF_SetFontOutline(font, (int)val)) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } +#else + TTF_SetFontOutline(font, (int)val); +#endif + return 0; +} + static PyObject * font_getter_name(PyObject *self, void *closure) { @@ -1168,6 +1209,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, diff --git a/test/font_test.py b/test/font_test.py index 9e906eda5e..75bb985209 100644 --- a/test/font_test.py +++ b/test/font_test.py @@ -685,6 +685,37 @@ 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) + + # 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_font_name(self): f = pygame_font.Font(None, 20) self.assertEqual(f.name, "FreeSans") @@ -933,6 +964,7 @@ def test_font_method_should_raise_exception_after_quit(self): ] skip_methods = set() version = pygame.font.get_sdl_ttf_version() + if version >= (2, 0, 18): methods.append(("get_point_size", ())) methods.append(("set_point_size", (34,))) @@ -1020,6 +1052,7 @@ def test_font_property_should_raise_exception_after_quit(self): ("italic", True), ("underline", True), ("strikethrough", True), + ("outline", 1), ] skip_properties = set() version = pygame.font.get_sdl_ttf_version() @@ -1096,6 +1129,7 @@ def query( underline=False, strikethrough=False, antialiase=False, + outline=0, ): if self.aborted: return False @@ -1106,7 +1140,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 = [] @@ -1120,11 +1154,14 @@ 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.outline = outline s = f.render(text, antialiase, (0, 0, 0)) screen.blit(s, (offset, y)) y += s.get_size()[1] + spacing @@ -1132,6 +1169,7 @@ def query( f.set_italic(False) f.set_underline(False) f.set_strikethrough(False) + f.outline = 0 s = f.render("(some comparison text)", False, (0, 0, 0)) screen.blit(s, (offset, y)) pygame.display.flip() @@ -1173,6 +1211,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()