Edit on GitHub

sysmon_pytk.font_utils

Font utilities.

  1# SPDX-FileCopyrightText: © 2024 Stacey Adams <stacey.belle.rose@gmail.com>
  2# SPDX-License-Identifier: MIT
  3
  4"""
  5Font utilities.
  6"""
  7
  8from __future__ import annotations
  9
 10import dataclasses
 11from enum import Enum
 12from tkinter import font
 13from tkinter.font import Font
 14from typing import Literal, TypeVar
 15
 16from typing_extensions import Self, TypeAlias
 17
 18from .app_locale import get_translator
 19
 20_ = get_translator()
 21
 22MAIN_FONT_FAMILY = "Source Sans Pro"
 23FIXED_FONT_FAMILY = "Source Code Pro"
 24
 25T = TypeVar("T")
 26
 27FontWeight: TypeAlias = Literal["normal", "bold"]
 28"""Font weight, one of `normal`, `bold`. For use with tk.Font."""
 29
 30FontSlant: TypeAlias = Literal["roman", "italic"]
 31"""Font slant, one of `roman`, `italic`. For use with tk.Font."""
 32
 33
 34class FontStyle(Enum):
 35    """
 36    Font style.
 37    """
 38
 39    REGULAR = "r"
 40    """Font style: Regular."""
 41
 42    BOLD = "b"
 43    """Font style: Bold."""
 44
 45    ITALIC = "i"
 46    """Font style: Italic."""
 47
 48    BOLD_ITALIC = "bi"
 49    """Font style: Bold Italic."""
 50
 51    @classmethod
 52    def _missing_(cls, value: object) -> Self | None:
 53        for member in cls:
 54            if member.value == value:
 55                return member
 56        return None
 57
 58    def is_bold(self) -> bool:
 59        """
 60        Determine whether the font style is bold.
 61        """
 62        return "b" in self.value
 63
 64    def is_italic(self) -> bool:
 65        """
 66        Determine whether the font style is italc.
 67        """
 68        return "i" in self.value
 69
 70    def get_weight(self) -> FontWeight:
 71        """
 72        Get the weight of the font style, either `normal` or `bold`.
 73        """
 74        return "bold" if self.is_bold() else "normal"
 75
 76    def get_slant(self) -> FontSlant:
 77        """
 78        Get the slant of the font style, either `roman` or `italic`.
 79        """
 80        return "italic" if self.is_italic() else "roman"
 81
 82    def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]:
 83        """
 84        Convert the font style to a weight and slant, for use with tk.Font.
 85        """
 86        weight: FontWeight = "normal"
 87        slant: FontSlant = "roman"
 88        if self == FontStyle.BOLD:
 89            weight = "bold"
 90        elif self == FontStyle.ITALIC:
 91            slant = "italic"
 92        elif self == FontStyle.BOLD_ITALIC:
 93            weight = "bold"
 94            slant = "italic"
 95        return (weight, slant)
 96
 97    @classmethod
 98    def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle:
 99        """
100        Given a weight and slant, return a FontStyle.
101
102        Parameters
103        ----------
104        weight : FontWeight
105            Font weight, one of `normal`, `bold`.
106        slant : FontSlant
107            Font slant, one of `roman`, `italic`.
108        """
109        if weight == "bold":
110            if slant == "italic":
111                return cls.BOLD_ITALIC
112            return cls.BOLD
113        if slant == "italic":
114            return cls.ITALIC
115        return cls.REGULAR
116
117
118@dataclasses.dataclass
119class FontDescription:
120    """
121    Font data, like what is returned by the `actual` method of a `Font` object.
122    """
123
124    family: str
125    """The font family name."""
126    size: int
127    """The font size."""
128    weight: FontWeight
129    """The font weight."""
130    slant: FontSlant
131    """The font slant."""
132    underline: bool
133    """Whether the font uses underline."""
134    overstrike: bool
135    """Whether the font uses strikethrough."""
136
137    def get_font(self) -> Font:
138        """
139        Return a Tk font based on this object's data.
140        """
141        return Font(
142            family=self.family, size=self.size, weight=self.weight,
143            slant=self.slant, underline=self.underline, overstrike=self.overstrike
144        )
145
146    def get_string(self) -> str:
147        """
148        Get the string which describes the font.
149        """
150        style = self.get_style()
151        style_text = ""
152        if style == FontStyle.BOLD:
153            style_text = _("Bold")
154        elif style == FontStyle.ITALIC:
155            style_text = _("Italic")
156        elif style == FontStyle.BOLD_ITALIC:
157            style_text = _("Bold Italic")
158        effects = self.get_effects()
159        return " ".join(f"{self.family} {style_text} {effects} {self.size}".split())
160
161    def get_style(self) -> FontStyle:
162        """
163        Get the font style, based on its slant and weight.
164        """
165        return FontStyle.from_weight_and_slant(self.weight, self.slant)
166
167    def get_effects(self) -> str:
168        """
169        Get the font effects (underline/strikethrough).
170        """
171        underline = _("Underline") if self.underline else ""
172        overstrike = _("Overstrike") if self.overstrike else ""
173        return f"{underline} {overstrike}".strip()
174
175
176def modify_named_font(  # pylint: disable=too-many-arguments
177    font_name: str, *,
178    size: int | None = None,
179    weight: FontWeight | None = None,
180    slant: FontSlant | None = None,
181    underline: bool | None = None,
182    overstrike: bool | None = None
183) -> Font:
184    """
185    Modify a named font by optionally changing weight, size, slant, etc.
186
187    Parameters
188    ----------
189    font_name : str
190        The name of the font to use
191    size : int, optional
192        The font size to use
193    weight : FontWeight, optional
194        The font weight to use
195    slant : FontSlant, optional
196        The font slant to use
197    underline : bool, optional
198        Whether the font should be underlined
199    overstrike : bool, optional
200        Whether the font should have strikethrough
201
202    Returns
203    -------
204    Font
205        A new font, with the parameters given.
206
207    Example
208    -------
209    >>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual()
210    {   'family': 'Bitstream Vera Sans',
211        'size': 13,
212        'weight': 'bold',
213        'slant': 'roman',
214        'underline': 0,
215        'overstrike': 0 }
216    """
217    if font_name in font.names():
218        fnt = font.nametofont(font_name).actual()
219        return Font(
220            family=fnt["family"],
221            size=get_with_fallback(size, fnt["size"]),
222            weight=get_with_fallback(weight, fnt["weight"]),
223            slant=get_with_fallback(slant, fnt["slant"]),
224            underline=get_with_fallback(underline, fnt["underline"]),
225            overstrike=get_with_fallback(overstrike, fnt["overstrike"])
226        )
227    return Font(name="TkDefaultFont")
228
229
230def get_with_fallback(value: T | None, fallback: T) -> T:
231    """
232    Return the value provided unless it is None. In that case, return the fallback.
233
234    Parameters
235    ----------
236    value : Optional[T]
237        Any value, possibly None
238    fallback : T
239        A fallback value, cannot be None
240
241    Returns
242    -------
243    T
244        A value that is not None
245    """
246    return value if value is not None else fallback
FontWeight: TypeAlias = Literal["normal", "bold"]

Font weight, one of normal, bold. For use with tk.Font.

FontSlant: TypeAlias = Literal["roman", "italic"]

Font slant, one of roman, italic. For use with tk.Font.

enum FontStyle(enum.Enum):
 35class FontStyle(Enum):
 36    """
 37    Font style.
 38    """
 39
 40    REGULAR = "r"
 41    """Font style: Regular."""
 42
 43    BOLD = "b"
 44    """Font style: Bold."""
 45
 46    ITALIC = "i"
 47    """Font style: Italic."""
 48
 49    BOLD_ITALIC = "bi"
 50    """Font style: Bold Italic."""
 51
 52    @classmethod
 53    def _missing_(cls, value: object) -> Self | None:
 54        for member in cls:
 55            if member.value == value:
 56                return member
 57        return None
 58
 59    def is_bold(self) -> bool:
 60        """
 61        Determine whether the font style is bold.
 62        """
 63        return "b" in self.value
 64
 65    def is_italic(self) -> bool:
 66        """
 67        Determine whether the font style is italc.
 68        """
 69        return "i" in self.value
 70
 71    def get_weight(self) -> FontWeight:
 72        """
 73        Get the weight of the font style, either `normal` or `bold`.
 74        """
 75        return "bold" if self.is_bold() else "normal"
 76
 77    def get_slant(self) -> FontSlant:
 78        """
 79        Get the slant of the font style, either `roman` or `italic`.
 80        """
 81        return "italic" if self.is_italic() else "roman"
 82
 83    def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]:
 84        """
 85        Convert the font style to a weight and slant, for use with tk.Font.
 86        """
 87        weight: FontWeight = "normal"
 88        slant: FontSlant = "roman"
 89        if self == FontStyle.BOLD:
 90            weight = "bold"
 91        elif self == FontStyle.ITALIC:
 92            slant = "italic"
 93        elif self == FontStyle.BOLD_ITALIC:
 94            weight = "bold"
 95            slant = "italic"
 96        return (weight, slant)
 97
 98    @classmethod
 99    def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle:
100        """
101        Given a weight and slant, return a FontStyle.
102
103        Parameters
104        ----------
105        weight : FontWeight
106            Font weight, one of `normal`, `bold`.
107        slant : FontSlant
108            Font slant, one of `roman`, `italic`.
109        """
110        if weight == "bold":
111            if slant == "italic":
112                return cls.BOLD_ITALIC
113            return cls.BOLD
114        if slant == "italic":
115            return cls.ITALIC
116        return cls.REGULAR

Font style.

REGULAR = <FontStyle.REGULAR: 'r'>

Font style: Regular.

BOLD = <FontStyle.BOLD: 'b'>

Font style: Bold.

ITALIC = <FontStyle.ITALIC: 'i'>

Font style: Italic.

BOLD_ITALIC = <FontStyle.BOLD_ITALIC: 'bi'>

Font style: Bold Italic.

def is_bold(self) -> bool:
59    def is_bold(self) -> bool:
60        """
61        Determine whether the font style is bold.
62        """
63        return "b" in self.value

Determine whether the font style is bold.

def is_italic(self) -> bool:
65    def is_italic(self) -> bool:
66        """
67        Determine whether the font style is italc.
68        """
69        return "i" in self.value

Determine whether the font style is italc.

def get_weight(self) -> sysmon_pytk.font_utils.FontWeight:
71    def get_weight(self) -> FontWeight:
72        """
73        Get the weight of the font style, either `normal` or `bold`.
74        """
75        return "bold" if self.is_bold() else "normal"

Get the weight of the font style, either normal or bold.

def get_slant(self) -> sysmon_pytk.font_utils.FontSlant:
77    def get_slant(self) -> FontSlant:
78        """
79        Get the slant of the font style, either `roman` or `italic`.
80        """
81        return "italic" if self.is_italic() else "roman"

Get the slant of the font style, either roman or italic.

def to_weight_and_slant( self) -> tuple[sysmon_pytk.font_utils.FontWeight, sysmon_pytk.font_utils.FontSlant]:
83    def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]:
84        """
85        Convert the font style to a weight and slant, for use with tk.Font.
86        """
87        weight: FontWeight = "normal"
88        slant: FontSlant = "roman"
89        if self == FontStyle.BOLD:
90            weight = "bold"
91        elif self == FontStyle.ITALIC:
92            slant = "italic"
93        elif self == FontStyle.BOLD_ITALIC:
94            weight = "bold"
95            slant = "italic"
96        return (weight, slant)

Convert the font style to a weight and slant, for use with tk.Font.

@classmethod
def from_weight_and_slant( cls, weight: sysmon_pytk.font_utils.FontWeight, slant: sysmon_pytk.font_utils.FontSlant) -> FontStyle:
 98    @classmethod
 99    def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle:
100        """
101        Given a weight and slant, return a FontStyle.
102
103        Parameters
104        ----------
105        weight : FontWeight
106            Font weight, one of `normal`, `bold`.
107        slant : FontSlant
108            Font slant, one of `roman`, `italic`.
109        """
110        if weight == "bold":
111            if slant == "italic":
112                return cls.BOLD_ITALIC
113            return cls.BOLD
114        if slant == "italic":
115            return cls.ITALIC
116        return cls.REGULAR

Given a weight and slant, return a FontStyle.

Parameters
  • weight (FontWeight): Font weight, one of normal, bold.
  • slant (FontSlant): Font slant, one of roman, italic.
Inherited Members
enum.Enum
name
value
@dataclasses.dataclass
class FontDescription:
119@dataclasses.dataclass
120class FontDescription:
121    """
122    Font data, like what is returned by the `actual` method of a `Font` object.
123    """
124
125    family: str
126    """The font family name."""
127    size: int
128    """The font size."""
129    weight: FontWeight
130    """The font weight."""
131    slant: FontSlant
132    """The font slant."""
133    underline: bool
134    """Whether the font uses underline."""
135    overstrike: bool
136    """Whether the font uses strikethrough."""
137
138    def get_font(self) -> Font:
139        """
140        Return a Tk font based on this object's data.
141        """
142        return Font(
143            family=self.family, size=self.size, weight=self.weight,
144            slant=self.slant, underline=self.underline, overstrike=self.overstrike
145        )
146
147    def get_string(self) -> str:
148        """
149        Get the string which describes the font.
150        """
151        style = self.get_style()
152        style_text = ""
153        if style == FontStyle.BOLD:
154            style_text = _("Bold")
155        elif style == FontStyle.ITALIC:
156            style_text = _("Italic")
157        elif style == FontStyle.BOLD_ITALIC:
158            style_text = _("Bold Italic")
159        effects = self.get_effects()
160        return " ".join(f"{self.family} {style_text} {effects} {self.size}".split())
161
162    def get_style(self) -> FontStyle:
163        """
164        Get the font style, based on its slant and weight.
165        """
166        return FontStyle.from_weight_and_slant(self.weight, self.slant)
167
168    def get_effects(self) -> str:
169        """
170        Get the font effects (underline/strikethrough).
171        """
172        underline = _("Underline") if self.underline else ""
173        overstrike = _("Overstrike") if self.overstrike else ""
174        return f"{underline} {overstrike}".strip()

Font data, like what is returned by the actual method of a Font object.

family: str

The font family name.

size: int

The font size.

The font weight.

The font slant.

underline: bool

Whether the font uses underline.

overstrike: bool

Whether the font uses strikethrough.

def get_font(self) -> tkinter.font.Font:
138    def get_font(self) -> Font:
139        """
140        Return a Tk font based on this object's data.
141        """
142        return Font(
143            family=self.family, size=self.size, weight=self.weight,
144            slant=self.slant, underline=self.underline, overstrike=self.overstrike
145        )

Return a Tk font based on this object's data.

def get_string(self) -> str:
147    def get_string(self) -> str:
148        """
149        Get the string which describes the font.
150        """
151        style = self.get_style()
152        style_text = ""
153        if style == FontStyle.BOLD:
154            style_text = _("Bold")
155        elif style == FontStyle.ITALIC:
156            style_text = _("Italic")
157        elif style == FontStyle.BOLD_ITALIC:
158            style_text = _("Bold Italic")
159        effects = self.get_effects()
160        return " ".join(f"{self.family} {style_text} {effects} {self.size}".split())

Get the string which describes the font.

def get_style(self) -> FontStyle:
162    def get_style(self) -> FontStyle:
163        """
164        Get the font style, based on its slant and weight.
165        """
166        return FontStyle.from_weight_and_slant(self.weight, self.slant)

Get the font style, based on its slant and weight.

def get_effects(self) -> str:
168    def get_effects(self) -> str:
169        """
170        Get the font effects (underline/strikethrough).
171        """
172        underline = _("Underline") if self.underline else ""
173        overstrike = _("Overstrike") if self.overstrike else ""
174        return f"{underline} {overstrike}".strip()

Get the font effects (underline/strikethrough).

def modify_named_font( font_name: str, *, size: int | None = None, weight: Optional[sysmon_pytk.font_utils.FontWeight] = None, slant: Optional[sysmon_pytk.font_utils.FontSlant] = None, underline: bool | None = None, overstrike: bool | None = None) -> tkinter.font.Font:
177def modify_named_font(  # pylint: disable=too-many-arguments
178    font_name: str, *,
179    size: int | None = None,
180    weight: FontWeight | None = None,
181    slant: FontSlant | None = None,
182    underline: bool | None = None,
183    overstrike: bool | None = None
184) -> Font:
185    """
186    Modify a named font by optionally changing weight, size, slant, etc.
187
188    Parameters
189    ----------
190    font_name : str
191        The name of the font to use
192    size : int, optional
193        The font size to use
194    weight : FontWeight, optional
195        The font weight to use
196    slant : FontSlant, optional
197        The font slant to use
198    underline : bool, optional
199        Whether the font should be underlined
200    overstrike : bool, optional
201        Whether the font should have strikethrough
202
203    Returns
204    -------
205    Font
206        A new font, with the parameters given.
207
208    Example
209    -------
210    >>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual()
211    {   'family': 'Bitstream Vera Sans',
212        'size': 13,
213        'weight': 'bold',
214        'slant': 'roman',
215        'underline': 0,
216        'overstrike': 0 }
217    """
218    if font_name in font.names():
219        fnt = font.nametofont(font_name).actual()
220        return Font(
221            family=fnt["family"],
222            size=get_with_fallback(size, fnt["size"]),
223            weight=get_with_fallback(weight, fnt["weight"]),
224            slant=get_with_fallback(slant, fnt["slant"]),
225            underline=get_with_fallback(underline, fnt["underline"]),
226            overstrike=get_with_fallback(overstrike, fnt["overstrike"])
227        )
228    return Font(name="TkDefaultFont")

Modify a named font by optionally changing weight, size, slant, etc.

Parameters
  • font_name (str): The name of the font to use
  • size (int, optional): The font size to use
  • weight (FontWeight, optional): The font weight to use
  • slant (FontSlant, optional): The font slant to use
  • underline (bool, optional): Whether the font should be underlined
  • overstrike (bool, optional): Whether the font should have strikethrough
Returns
  • Font: A new font, with the parameters given.
Example
>>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual()
{   'family': 'Bitstream Vera Sans',
    'size': 13,
    'weight': 'bold',
    'slant': 'roman',
    'underline': 0,
    'overstrike': 0 }
def get_with_fallback(value: Optional[~T], fallback: ~T) -> ~T:
231def get_with_fallback(value: T | None, fallback: T) -> T:
232    """
233    Return the value provided unless it is None. In that case, return the fallback.
234
235    Parameters
236    ----------
237    value : Optional[T]
238        Any value, possibly None
239    fallback : T
240        A fallback value, cannot be None
241
242    Returns
243    -------
244    T
245        A value that is not None
246    """
247    return value if value is not None else fallback

Return the value provided unless it is None. In that case, return the fallback.

Parameters
  • value (Optional[T]): Any value, possibly None
  • fallback (T): A fallback value, cannot be None
Returns
  • T: A value that is not None