Skip to content

Commit fc7592e

Browse files
Add Font Manager and update docs
1 parent be572aa commit fc7592e

5 files changed

Lines changed: 231 additions & 38 deletions

File tree

docs/frameworks/app-manager.md

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -331,49 +331,31 @@ else:
331331

332332
### Script Execution
333333

334-
AppManager can execute arbitrary Python scripts with proper environment setup:
334+
AppManager can execute an app entrypoint module with proper environment setup:
335335

336336
```python
337337
from mpos import AppManager
338338

339339
# Execute a script file
340340
success = AppManager.execute_script(
341341
script_source="assets/main.py",
342-
is_file=True,
343342
classname="Main",
344343
cwd="apps/com.example.myapp/assets/"
345344
)
346-
347-
# Execute inline script
348-
success = AppManager.execute_script(
349-
script_source="print('Hello')",
350-
is_file=False,
351-
classname="Main"
352-
)
353345
```
354346

355347
### Execution Details
356348

357349
When executing a script, AppManager:
358350

359-
1. **Reads the file** (if `is_file=True`)
360-
2. **Compiles the script** to bytecode
361-
3. **Sets up globals** with LVGL and other imports
362-
4. **Executes the script** in the prepared environment
363-
5. **Finds the main activity class** by name
364-
6. **Starts the activity** using the Activity framework
365-
7. **Restores sys.path** to clean up
366-
367-
The execution environment includes:
368-
369-
```python
370-
script_globals = {
371-
'lv': lv, # LVGL module
372-
'__name__': "__main__"
373-
}
374-
```
351+
1. **Adds app assets path** to the front of `sys.path` (if `cwd` provided)
352+
2. **Imports the module** derived from `script_source` filename
353+
3. **Lets MicroPython choose** `.py` first, then `.mpy` in the same directory
354+
4. **Finds the main activity class** by name
355+
5. **Starts the activity** using the Activity framework
356+
6. **Restores sys.path** and module cache entry (`sys.modules`) to clean up
375357

376-
If a `cwd` is provided, it's added to `sys.path` for relative imports.
358+
If a `cwd` is provided, it's temporarily placed first in `sys.path` for import resolution.
377359

378360
## Complete Example: App Store Integration
379361

@@ -560,25 +542,24 @@ Start an app by fullname.
560542
3. Executes main activity script
561543
4. Shows/hides top menu bar based on app type
562544

563-
**`execute_script(script_source, is_file, classname, cwd=None)`**
545+
**`execute_script(script_source, classname, cwd=None)`**
564546

565547
Execute a Python script with proper environment.
566548

567549
- **Parameters:**
568-
- `script_source` (str): Script path (if `is_file=True`) or script code
569-
- `is_file` (bool): `True` if script_source is a file path
550+
- `script_source` (str): Script path used to derive module name (e.g., `assets/main.py` -> `main`)
570551
- `classname` (str): Name of main activity class to instantiate
571552
- `cwd` (str, optional): Working directory to add to sys.path
572553

573554
- **Returns:** `bool` - `True` if successful
574555

575556
- **Behavior:**
576-
1. Reads file (if `is_file=True`)
577-
2. Compiles script to bytecode
578-
3. Executes in prepared environment
557+
1. Temporarily prioritizes `cwd` in `sys.path` (if provided)
558+
2. Imports module name derived from `script_source`
559+
3. Uses normal MicroPython import precedence (`.py` then `.mpy`)
579560
4. Finds and instantiates main activity class
580561
5. Starts activity using Activity framework
581-
6. Restores sys.path
562+
6. Restores `sys.path` and the previous `sys.modules` entry for that module name
582563

583564
### Intent Resolution
584565

docs/frameworks/font-manager.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# FontManager
2+
3+
FontManager is a singleton framework for loading, caching, and composing LVGL fonts — including built-in bitmap fonts, TrueType fonts, and emoji image fonts. It automatically selects the best-quality pre-rendered emoji asset for the requested size and uses LVGL's imgfont fallback mechanism to render emoji inline with text.
4+
5+
## Overview
6+
7+
FontManager centralizes all font concerns in a single class:
8+
9+
- **Unified API** - One call (`getFont`) to get any font, with or without emoji support
10+
- **Emoji Compositing** - Transparently layers an emoji imgfont on top of any base font via LVGL's `fallback` mechanism
11+
- **Size-tiered Assets** - Picks the closest pre-rendered emoji PNG directory so LVGL never has to scale further than necessary
12+
- **Lazy Caching** - Fonts and scaled image descriptors are cached on first use; no redundant work on subsequent calls
13+
- **Android-Inspired** - Follows the same singleton/class-method pattern as other MicroPythonOS frameworks
14+
15+
## Quick Start
16+
17+
```python
18+
from mpos import FontManager
19+
20+
# Get a built-in Montserrat font at 16px, with emoji support (default)
21+
font = FontManager.getFont(size=16, family="Montserrat")
22+
23+
# Get the same font without emoji (e.g. for glyph enumeration)
24+
base_font = FontManager.getFont(size=16, family="Montserrat", emoji=False)
25+
26+
# Load a TrueType font from a file
27+
ttf_font = FontManager.getFont(size=42, ttf="M:apps/com.myapp/assets/MyFont.ttf")
28+
29+
# List all available built-in fonts (each already has emoji composed in)
30+
for info in FontManager.listFonts():
31+
print(info["name"], info["size"])
32+
33+
# Get all available emoji codepoints (union across all size tiers)
34+
for cp in FontManager.getEmojiCodepoints():
35+
print(hex(cp))
36+
```
37+
38+
## Architecture
39+
40+
FontManager is implemented as a singleton using class variables and class methods. No instance creation is needed.
41+
42+
### Font composition
43+
44+
When `emoji=True` (the default), `getFont()` wraps the requested base font in an LVGL imgfont that renders emoji as scaled PNG images. The imgfont's `fallback` is set to the base font, so LVGL automatically falls through to the base font for any codepoint that is not an emoji.
45+
46+
```
47+
getFont(size=16)
48+
└── base_font = font_montserrat_16 (builtin bitmap)
49+
└── emoji_font = lv.imgfont_create(...) (PNG-based imgfont)
50+
└── emoji_font.fallback = base_font
51+
└── returns emoji_font
52+
```
53+
54+
### Emoji size tiers
55+
56+
Emoji PNGs are stored in size-specific directories under `builtin/res/emojis/`:
57+
58+
| Directory | Max target height | Used for |
59+
|-----------|------------------|----------|
60+
| `20x20/` | ≤ 20 px | Montserrat 8–16 (line heights ≤ 20 px) |
61+
| `56x56/` | any | Larger fonts |
62+
63+
`_imgfont_path_cb` receives the rendering font's pixel height and picks the smallest tier whose `max_height` is ≥ the target. This minimises (or eliminates) the nearest-neighbour downscale performed by LVGL's software renderer.
64+
65+
### Caching layers
66+
67+
| Cache | Key | Value |
68+
|-------|-----|-------|
69+
| `_composed_font_cache` | `(font_id, emoji_size)` | Composed imgfont object |
70+
| `_ttf_font_cache` | `(path, size)` | `lv.tiny_ttf_create_file` result |
71+
| `_emoji_maps` | `dir_name` | `{codepoint: src_path}` dict |
72+
| `_imgfont_scaled_src_cache` | `(src, target_height)` | Scaled `lv.image_dsc_t` or original src |
73+
| `_imgfont_source_size_cache` | `src` | `(width, height)` tuple |
74+
| `_imgfont_empty_src_cache` | `target_height` | 1×h transparent `lv.image_dsc_t` |
75+
76+
## API Reference
77+
78+
### `getFont(size=None, ttf=None, family=None, emoji=True)`
79+
80+
Return a font object suitable for use with `set_style_text_font()`.
81+
82+
**Parameters:**
83+
84+
- `size` (int, optional): Target pixel size. Snapped to the nearest available builtin size. Defaults to 12.
85+
- `ttf` (str, optional): Path to a `.ttf` file (e.g. `"M:apps/myapp/assets/MyFont.ttf"`). When provided, `family` is ignored.
86+
- `family` (str, optional): Font family name — `"Montserrat"` (default) or `"Unscii"`.
87+
- `emoji` (bool): If `True` (default), the returned font transparently renders emoji via a PNG imgfont fallback. Pass `False` to get the raw base font (useful for `get_glyph_dsc` enumeration).
88+
89+
**Returns:** `lv.font_t` — the requested font, possibly wrapped with emoji support.
90+
91+
**Example:**
92+
93+
```python
94+
from mpos import FontManager
95+
import lvgl as lv
96+
97+
font = FontManager.getFont(size=24, family="Montserrat")
98+
label = lv.label(screen)
99+
label.set_style_text_font(font, lv.PART.MAIN)
100+
label.set_text("Hello ❤️ 😀")
101+
```
102+
103+
---
104+
105+
### `listFonts()`
106+
107+
Return a list of all available built-in fonts, each already composed with emoji support.
108+
109+
**Returns:** list of dicts, each with keys:
110+
111+
- `"name"` (str): Human-readable name, e.g. `"Montserrat 16"`
112+
- `"family"` (str): Family name, e.g. `"Montserrat"`
113+
- `"size"` (int): Nominal point size
114+
- `"font"` (lv.font_t): Composed font (with emoji)
115+
- `"base_font"` (lv.font_t): Raw base font (without emoji)
116+
117+
**Example:**
118+
119+
```python
120+
from mpos import FontManager
121+
import lvgl as lv
122+
123+
for info in FontManager.listFonts():
124+
label = lv.label(screen)
125+
label.set_style_text_font(info["font"], lv.PART.MAIN)
126+
label.set_text(info["name"] + ": ABC 😀 ❤️")
127+
```
128+
129+
---
130+
131+
### `getEmojiCodepoints()`
132+
133+
Return a sorted list of all emoji codepoints available across all size tiers.
134+
135+
**Returns:** list of int
136+
137+
**Example:**
138+
139+
```python
140+
from mpos import FontManager
141+
142+
for cp in FontManager.getEmojiCodepoints():
143+
print(hex(cp), chr(cp))
144+
```
145+
146+
---
147+
148+
### `normalizeEmojiText(text)`
149+
150+
Strip Unicode variation selectors (U+FE0E text selector, U+FE0F emoji selector) from a string. Useful before storing or comparing text that may have been pasted from a source that appends these codepoints.
151+
152+
**Parameters:**
153+
154+
- `text` (str): Input string
155+
156+
**Returns:** str — cleaned string
157+
158+
**Example:**
159+
160+
```python
161+
from mpos import FontManager
162+
163+
clean = FontManager.normalizeEmojiText("❤️") # removes U+FE0F after ❤
164+
```
165+
166+
## Emoji Assets
167+
168+
Emoji PNGs are stored in `internal_filesystem/builtin/res/emojis/` and are included in the firmware image at `/builtin/res/emojis/` on the device filesystem. Each subdirectory is a size tier:
169+
170+
```
171+
builtin/res/emojis/
172+
├── 20x20/ # Pre-rendered at 20×20 px (Lanczos-downscaled from 56×56)
173+
│ ├── 1F600.png
174+
│ ├── 263A.png
175+
│ └── ...
176+
└── 56x56/ # Cropped from original 72×72 OpenMoji PNGs
177+
├── 1F600.png
178+
├── 263A.png
179+
└── ...
180+
```
181+
182+
Files are named by their Unicode codepoint in uppercase hex (e.g. `1F600.png` for 😀). FontManager scans each directory at runtime and builds a `{codepoint: path}` map.
183+
184+
### Adding new emoji
185+
186+
1. Add a PNG named `<CODEPOINT_HEX>.png` to both `56x56/` and `20x20/` (generate the 20×20 version with ImageMagick):
187+
188+
```bash
189+
convert original.png -gravity center -crop 56x56+0+0 +repage -filter Lanczos -resize 20x20 20x20/CODEPOINT.png
190+
```
191+
192+
2. The new emoji will be picked up automatically on next boot — no code changes needed.
193+
194+
## File Structure
195+
196+
```
197+
internal_filesystem/
198+
├── lib/mpos/ui/
199+
│ └── font_manager.py # FontManager class
200+
└── builtin/res/emojis/
201+
├── 20x20/ # 20×20 px emoji PNGs
202+
└── 56x56/ # 56×56 px emoji PNGs
203+
```
204+
205+
## Related Frameworks
206+
207+
- **[AppearanceManager](appearance-manager.md)** - Theme colors and light/dark mode
208+
- **[DisplayMetrics](display-metrics.md)** - Display width, height, DPI
209+
210+
## See Also
211+
212+
- [Architecture Overview](../architecture/overview.md)
213+
- [Creating Apps](../apps/creating-apps.md)

docs/frameworks/input-manager.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ InputManager centralizes all input-related operations in a single class with cla
99
- **Unified API** - Single class for all input management
1010
- **Clean Namespace** - No scattered functions cluttering imports
1111
- **Testable** - InputManager can be tested independently
12-
- **Focus Control** - Emulate focus on specific UI objects
1312
- **Pointer Access** - Get current touch/pointer coordinates
1413
- **Device Registration** - Register and query available input devices by type
1514

@@ -66,12 +65,11 @@ if InputManager.has_pointer():
6665
### Managing Focus
6766

6867
```python
69-
from mpos import InputManager
7068
import lvgl as lv
7169

7270
focusgroup = lv.group_get_default()
7371
if focusgroup:
74-
InputManager.emulate_focus_obj(focusgroup, my_button)
72+
lv.group_focus_obj(my_button)
7573
```
7674

7775
## API Reference
@@ -89,7 +87,7 @@ Check if any registered input device is a pointer/touch device.
8987
**Returns:** bool - True if a pointer device is registered
9088

9189
#### `emulate_focus_obj(focusgroup, target)`
92-
Emulate setting focus to a specific object in the focus group.
90+
Deprecated compatibility shim. Use `lv.group_focus_obj(target)` directly.
9391

9492
#### `register_indev(indev)`
9593
Register an input device for later querying by type.

docs/frameworks/preferences.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Here's a simple example of how to add it to your app, taken from [QuasiNametag](
2626
self.create_display_screen(container)
2727
@@ -263,6 +270,13 @@
2828
if focusgroup:
29-
mpos.ui.focus_direction.emulate_focus_obj(focusgroup, self.display_screen)
29+
lv.group_focus_obj(self.display_screen)
3030

3131
+ print("Saving preferences...")
3232
+ editor = mpos.config.SharedPreferences("com.quasikili.quasinametag").edit()

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ nav:
5858
- ConnectivityManager: frameworks/connectivity-manager.md
5959
- DeviceInfo: frameworks/device-info.md
6060
- DisplayMetrics: frameworks/display-metrics.md
61+
- FontManager: frameworks/font-manager.md
6162
- DownloadManager: frameworks/download-manager.md
6263
- InputManager: frameworks/input-manager.md
6364
- LightsManager: frameworks/lights-manager.md

0 commit comments

Comments
 (0)