diff --git a/pretty_gpx/common/drawing/utils/drawing_figure.py b/pretty_gpx/common/drawing/utils/drawing_figure.py index 0fad36c..7583ca3 100644 --- a/pretty_gpx/common/drawing/utils/drawing_figure.py +++ b/pretty_gpx/common/drawing/utils/drawing_figure.py @@ -33,6 +33,14 @@ def __call__(self, paper_size: PaperSize) -> float: scale = paper_size.diag_mm/PAPER_SIZES['A4'].diag_mm return mm_to_point(self.__val_mm)*scale + def __mul__(self, other: float) -> 'A4Float': + """Multiply the A4Float by a scalar.""" + return A4Float(mm=self.__val_mm * other) + + def __truediv__(self, other: float) -> 'A4Float': + """Divide the A4Float by a scalar.""" + return A4Float(mm=self.__val_mm / other) + class MetersFloat: """Scales a meter measurement to points based on paper size and GPX bounds.""" diff --git a/pretty_gpx/common/drawing/utils/fonts.py b/pretty_gpx/common/drawing/utils/fonts.py index 3b75118..3b9d3de 100644 --- a/pretty_gpx/common/drawing/utils/fonts.py +++ b/pretty_gpx/common/drawing/utils/fonts.py @@ -1,9 +1,7 @@ #!/usr/bin/python3 """Fonts.""" import os -import textwrap from enum import Enum -from pathlib import Path from matplotlib.font_manager import FontProperties @@ -23,25 +21,3 @@ class CustomFont(Enum): def font_name(self) -> str: """Get the font name.""" return self.value.get_name() - - def get_css_header(self) -> str | None: - """Get the CSS header for the font.""" - font_path = self.value.get_file() - if font_path is None or not isinstance(font_path, str): - return None - - font_path = Path(font_path).name - if font_path.lower().endswith('.otf'): - font_format = 'opentype' - elif font_path.lower().endswith('.ttf'): - font_format = 'truetype' - else: - raise ValueError("Unsupported font format. Please provide a .otf or .ttf file.") - - header = f''' - @font-face {{ - font-family: '{self.font_name}'; - src: url('/fonts/{font_path}') format('{font_format}'); - }} - ''' - return textwrap.dedent(header) diff --git a/pretty_gpx/ui/pages/city/page.py b/pretty_gpx/ui/pages/city/page.py index ccb79b7..2e1daf7 100644 --- a/pretty_gpx/ui/pages/city/page.py +++ b/pretty_gpx/ui/pages/city/page.py @@ -61,8 +61,8 @@ def update_drawer_params(self) -> None: self.drawer.params.profile_fill_color = theme.track_color self.drawer.params.profile_font_color = theme.background_color self.drawer.params.centered_title_font_color = theme.point_color - - self.drawer.params.centered_title_fontproperties = self.font.value.value + self.drawer.params.centered_title_fontproperties = self.font.font.value + self.drawer.params.centered_title_font_size = self.font._current_fontsize for cat in [ScatterPointCategory.CITY_BRIDGE, ScatterPointCategory.CITY_POI_DEFAULT, diff --git a/pretty_gpx/ui/pages/mountain/page.py b/pretty_gpx/ui/pages/mountain/page.py index 82aa2e8..c4878fe 100644 --- a/pretty_gpx/ui/pages/mountain/page.py +++ b/pretty_gpx/ui/pages/mountain/page.py @@ -56,6 +56,8 @@ def update_drawer_params(self) -> None: self.drawer.params.profile_fill_color = theme.track_color self.drawer.params.profile_font_color = theme.background_color self.drawer.params.centered_title_font_color = theme.peak_color + self.drawer.params.centered_title_fontproperties = self.font.font.value + self.drawer.params.centered_title_font_size = self.font._current_fontsize for cat in [ScatterPointCategory.MOUNTAIN_PASS, ScatterPointCategory.START, diff --git a/pretty_gpx/ui/pages/multi_mountain/page.py b/pretty_gpx/ui/pages/multi_mountain/page.py index 5e9e084..7a2b656 100644 --- a/pretty_gpx/ui/pages/multi_mountain/page.py +++ b/pretty_gpx/ui/pages/multi_mountain/page.py @@ -65,6 +65,8 @@ def update_drawer_params(self) -> None: self.drawer.params.profile_fill_color = theme.track_color self.drawer.params.profile_font_color = theme.background_color self.drawer.params.centered_title_font_color = theme.peak_color + self.drawer.params.centered_title_fontproperties = self.font.font.value + self.drawer.params.centered_title_font_size = self.font._current_fontsize for cat in [ScatterPointCategory.MOUNTAIN_HUT, ScatterPointCategory.START, diff --git a/pretty_gpx/ui/pages/template/ui_font_and_size_select.py b/pretty_gpx/ui/pages/template/ui_font_and_size_select.py new file mode 100644 index 0000000..a5fe63d --- /dev/null +++ b/pretty_gpx/ui/pages/template/ui_font_and_size_select.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 +"""Ui Fonts Menu, to select a font from a list and also select a font size.""" +from collections.abc import Awaitable +from collections.abc import Callable + +from nicegui import ui + +from pretty_gpx.common.drawing.utils.drawing_figure import A4Float +from pretty_gpx.common.drawing.utils.fonts import CustomFont +from pretty_gpx.ui.pages.template.ui_font_select import UiFontSelect + + +class UiFontAndSizeSelect: + """NiceGui menu to select a font in a list and also select a font size.""" + + def __init__(self, *, + label: str, + fonts: tuple[CustomFont, ...], + on_change: Callable[[], Awaitable[None]], + start_fontsize: A4Float, + start_font: CustomFont | None = None, + fontsize_geometric_step: float = 1.2) -> None: + """Create a UiFontAndSizeSelect.""" + with ui.row().classes('items-center gap-2'): + font_select = UiFontSelect(label=label, + fonts=fonts, + on_change=on_change, + start_font=start_font) + + def on_click_minus() -> Callable[[], Awaitable[None]]: + """On click minus handler.""" + async def handler() -> None: + self._current_fontsize /= fontsize_geometric_step + await on_change() + return handler + + def on_click_plus() -> Callable[[], Awaitable[None]]: + """On click plus handler.""" + async def handler() -> None: + self._current_fontsize *= fontsize_geometric_step + await on_change() + return handler + + with ui.button(icon='remove', on_click=on_click_minus() + ).props('dense round').classes('bg-white text-black border border-black'): + ui.tooltip('Decrease font size') + with ui.button(icon='add', on_click=on_click_plus() + ).props('dense round').classes('bg-white text-black border border-black'): + ui.tooltip('Increase font size') + + ### + + self._font_select = font_select + self._current_fontsize = start_fontsize + + @property + def font(self) -> CustomFont: + """Return the selected font.""" + return self._font_select.font + + @property + def fontsize(self) -> A4Float: + """Return the selected fontsize.""" + return self._current_fontsize diff --git a/pretty_gpx/ui/pages/template/ui_font_select.py b/pretty_gpx/ui/pages/template/ui_font_select.py new file mode 100644 index 0000000..129850c --- /dev/null +++ b/pretty_gpx/ui/pages/template/ui_font_select.py @@ -0,0 +1,95 @@ +#!/usr/bin/python3 +"""Ui Fonts Menu, to select a font from a list.""" +import os +import textwrap +from collections.abc import Awaitable +from collections.abc import Callable + +from nicegui import app +from nicegui import ui + +from pretty_gpx.common.drawing.utils.fonts import CustomFont +from pretty_gpx.common.utils.paths import FONTS_DIR + +app.add_static_files('/fonts', os.path.abspath(FONTS_DIR)) + + +class UiFontSelect: + """NiceGui menu to select a font in a list.""" + + def __init__(self, + *, + label: str, + fonts: tuple[CustomFont, ...], + on_change: Callable[[], Awaitable[None]], + start_font: CustomFont | None = None) -> None: + """Create a UiFontsMenu.""" + if start_font is None: + current_idx = 0 + else: + current_idx = fonts.index(start_font) # Can raise ValueError if not found + + def on_click_idx(idx: int) -> Callable[[], Awaitable[None]]: + """On click handler.""" + async def handler() -> None: + self.change_current_idx(idx) + await on_change() + return handler + + with ui.dropdown_button(label, icon="font_download", auto_close=True) as main_button: + main_button.classes("bg-white text-black normal-case") + + for font in fonts: + font_css_header = get_css_header(font) + if font_css_header is not None: + ui.add_css(font_css_header) + + items = [ui.item(font.font_name, on_click=on_click_idx(idx)) + .style(f'font-family:"{font.font_name}";') + for idx, font in enumerate(fonts)] + + ### + + self.main_button = main_button + self.fonts = fonts + self.items = items + self.current_idx = current_idx + + self.change_current_idx(current_idx) + + def change_current_idx(self, new_idx: int) -> None: + """Change the current index.""" + if new_idx < 0 or new_idx >= len(self.fonts): + raise IndexError(f"Index {new_idx} out of bounds for fonts list of length {len(self.fonts)}.") + self.items[self.current_idx].classes(replace="bg-white text-black") + self.items[new_idx].classes(replace="bg-primary text-white") + self.current_idx = new_idx + self.main_button.style(f'font-family: "{self.font.font_name}";') + + @property + def font(self) -> CustomFont: + """Return the selected font.""" + return self.fonts[self.current_idx] + + +def get_css_header(font: CustomFont) -> str | None: + """Get the CSS header for the font.""" + font_path = font.value.get_file() + if font_path is None or not isinstance(font_path, str): + return None + + font_path = os.path.basename(font_path) + if font_path.lower().endswith('.otf'): + font_format = 'opentype' + elif font_path.lower().endswith('.ttf'): + font_format = 'truetype' + else: + raise ValueError("Unsupported font format. Please provide a .otf or .ttf file.") + + header = f''' + @font-face {{ + font-family: '{font.font_name}'; + src: url('/fonts/{font_path}') format('{font_format}'); + }} + ''' + return textwrap.dedent(header) diff --git a/pretty_gpx/ui/pages/template/ui_fonts_menu.py b/pretty_gpx/ui/pages/template/ui_fonts_menu.py deleted file mode 100644 index 1ec6893..0000000 --- a/pretty_gpx/ui/pages/template/ui_fonts_menu.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/python3 -"""Ui Fonts Menu, to select a font from a list.""" -import os -from collections.abc import Awaitable -from collections.abc import Callable -from dataclasses import dataclass -from typing import Self - -from nicegui import app -from nicegui import ui - -from pretty_gpx.common.drawing.utils.fonts import CustomFont -from pretty_gpx.common.utils.paths import FONTS_DIR - -app.add_static_files('/fonts', os.path.abspath(FONTS_DIR)) - - -@dataclass -class UiFontsMenu: - """NiceGui menu to select a font in a list.""" - - button: ui.dropdown_button - fonts: tuple[CustomFont, ...] - - @classmethod - def create(cls, - *, - fonts: tuple[CustomFont, ...], - tooltip: str, - on_change: Callable[[], Awaitable[None]], - start_font: CustomFont | None = None) -> Self: - """Create a UiFontsMenu.""" - if start_font is None: - start_font = fonts[0] - - with ui.dropdown_button(start_font.font_name, auto_close=True) as button: - button.tooltip(tooltip) - button.classes('bg-white text-black w-48 justify-start') - button.style(f'display: block; font-family: "{start_font.font_name}"; width: 100%;' - 'border: 1px solid #ddd; border-radius: 4px; position: relative;') - - for font in fonts: - font_css_header = font.get_css_header() - if font_css_header is not None: - ui.add_css(font_css_header) - - def create_click_handler(selected_font: str) -> Callable[[], Awaitable[None]]: - async def handler() -> None: - button.text = selected_font - button.style(f'font-family: "{selected_font}";') - await on_change() - return handler - - ui.item(font.font_name, on_click=create_click_handler(font.font_name)) \ - .style(f'display: block; font-family:"{font.font_name}"; width: 100%;' - 'border: 1px solid #ddd; border-radius: 4px; position: relative;') - - return cls(button, fonts) - - @property - def value(self) -> CustomFont: - """Return the selected font.""" - for f in self.fonts: - if f.font_name == self.button.text: - return f - raise ValueError(f"Selected font '{self.button.text}' not found in available fonts.") diff --git a/pretty_gpx/ui/pages/template/ui_manager.py b/pretty_gpx/ui/pages/template/ui_manager.py index 06cceec..340fd79 100644 --- a/pretty_gpx/ui/pages/template/ui_manager.py +++ b/pretty_gpx/ui/pages/template/ui_manager.py @@ -18,12 +18,13 @@ from pretty_gpx.common.drawing.utils.color_theme import LightTheme from pretty_gpx.common.drawing.utils.drawer import DrawerMultiTrack from pretty_gpx.common.drawing.utils.drawer import DrawerSingleTrack +from pretty_gpx.common.drawing.utils.drawing_figure import A4Float from pretty_gpx.common.drawing.utils.fonts import CustomFont from pretty_gpx.common.layout.paper_size import PAPER_SIZES from pretty_gpx.common.layout.paper_size import PaperSize from pretty_gpx.common.utils.logger import logger from pretty_gpx.common.utils.profile import profile_parallel -from pretty_gpx.ui.pages.template.ui_fonts_menu import UiFontsMenu +from pretty_gpx.ui.pages.template.ui_font_and_size_select import UiFontAndSizeSelect from pretty_gpx.ui.pages.template.ui_input import UiInputFloat from pretty_gpx.ui.pages.template.ui_input import UiInputStr from pretty_gpx.ui.pages.template.ui_plot import UiPlot @@ -126,7 +127,7 @@ class UiManager(Generic[T], ABC): paper_size: UiToggle[PaperSize] title: UiInputStr dist_km: UiInputFloat - font: UiFontsMenu + font: UiFontAndSizeSelect dark_mode_switch: ui.switch theme: UiToggle[DarkTheme] | UiToggle[LightTheme] @@ -206,17 +207,18 @@ def __init__(self, drawer: T) -> None: # with self.subclass_column: + self.font = UiFontAndSizeSelect(label="Title's Font", + fonts=(CustomFont.LOBSTER, + CustomFont.MONOTON, + CustomFont.GOCHI_HAND, + CustomFont.EMILIO_20, + CustomFont.ALLERTA_STENCIL), + start_fontsize=A4Float(mm=20), + on_change=self.on_click_update) self.title = UiInputStr.create(label='Title', value="Title", tooltip="Press Enter to update title", on_enter=self.on_click_update) self.dist_km = UiInputFloat.create(label='Distance (km)', value="", on_enter=self.on_click_update, tooltip="Press Enter to override distance from GPX") - self.font = UiFontsMenu.create(fonts=(CustomFont.LOBSTER, - CustomFont.MONOTON, - CustomFont.GOCHI_HAND, - CustomFont.EMILIO_20, - CustomFont.ALLERTA_STENCIL), - on_change=self.on_click_update, - tooltip="Select the title's font") # # New fields will be added here by the subclass