Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- macOS: OpenGL rendering backend removed in favor of Metal only
- `LottieReader::parseFile()`, `parseData()`, `parseStream()`, and `parseFromZip()` now return `ResultValue<AnimationComposition::Ptr>` and no longer take a trailing `String* outError` out-parameter; check `wasOk()`/`failed()` and read the message via `getErrorMessage()`.
- `AnimationFrameExporter` is now an instance-based class bound to a `GraphicsContext` (construct `AnimationFrameExporter exporter (ctx);` then call `exporter.renderFrame(anim, …)` / `exporter.renderAllFrames(…)` / `exporter.exportToGif(anim, …)`), so it can own and reuse the GPU matte-composite pipeline across frames instead of recompiling it per frame. The `exportToGif(frames, frameRate, …)` frame-sequence encoder remains a static helper.
- Config macro `YUP_EMBED_DEFAULT_THEME_TEXT_FONT` renamed to `YUP_EMBED_DEFAULT_THEME_TEXT_SERIF_FONT`; the embedded default text font now only covers the serif font. A new `YUP_EMBED_DEFAULT_THEME_TEXT_MONOSPACE_FONT` config selects whether the monospace theme font is embedded.
- `Font` loading is now static-only: the instance `loadFromData()` / `loadFromFile()` methods were removed in favor of `Font::loadFontFromData()`, `Font::loadFontFromFile()`, `Font::loadFontFromFirstAvailableFile()`, `Font::loadSerifSystemTextFont()` and `Font::loadMonospaceSystemTextFont()`, all returning `ResultValue<Font>`.

### Graphics

Expand Down Expand Up @@ -70,6 +72,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `ImageFormat::Options` struct controls metadata extraction: `.withMetadata(true)` enables text metadata and DPI; `.withRawChunks(true)` enables raw binary chunks (EXIF, ICC, XMP). When both are false (the default), `ImageMetadata` is not allocated — true zero overhead.
- Introduced a ref-counted `ImageMetadata` object (`ImageMetadata::Ptr`) attached to `Image` and `ImageFormatReader::metadata`. DPI, text entries, and raw binary chunks are all accessed through the metadata object only when requested via `Options`.
- Lossless roundtrip tests for all formats (BMP, PNG, WebP, TGA, TIFF, PPM, GIF) now verify pixel-perfect fidelity after write→read; animated roundtrip tests for GIF, WebP, and PNG verify per-frame pixel integrity.
- `StyledText::TextModifier::appendText()` gained a `Color` overload that creates (and caches per color) a solid fill paint, and `Graphics::fillFittedText()` now honors per-run style paints when every run carries one — enabling syntax-colored text. Single-color `StyledText` usage is unchanged. `Font` gained `isEmpty()`.
- New `Font` static loaders: `Font::loadFontFromData()`, `Font::loadFontFromFile()`, `Font::loadFontFromFirstAvailableFile()`, `Font::loadSerifSystemTextFont()` and `Font::loadMonospaceSystemTextFont()`, all returning `ResultValue<Font>` (`wasOk()` / `failed()` / `getValue()`). The former theme-local system font lookup helpers moved into `Font`; macOS/iOS use the CoreText system UI fonts, other platforms try well-known system font files.

### UI

- `ApplicationTheme` now exposes `setDefaultMonospaceFont()` / `getDefaultMonospaceFont()`, mirroring the existing default and icon font APIs. The default theme populates it from the embedded (or system) monospace font.
- The default theme now embeds JetBrains Mono Variable (SIL OFL) as its monospace font when `YUP_EMBED_DEFAULT_THEME_TEXT_MONOSPACE_FONT = 1` (forced on Emscripten), falling back to the system monospace font otherwise. The `tools/embed_font.py` regenerates the `.inc` byte arrays from any font file.
- New `CodeDocument` (line-based text model with `UndoManager`-backed edits, positions, and incremental change notifications), `SyntaxDefinition` (JSON-driven language descriptions loaded from data/files or the built-in C++ / GLSL / Python definitions), `CodeTokeniser` (incremental per-line tokenizer with a line-state machine for multi-line constructs and lazy re-tokenization), and the `CodeEditor` component: syntax-highlighted editing, caret/selection with anchor semantics, clipboard, undo/redo, read-only, smart auto-indent, an optional line-number gutter with breakpoint markers, find/replace (find-all, next/previous with wrap, replace-one, replace-all in one undo step, match highlighting), bracket matching, and an optional minimap overview. Defaults to the theme's monospace font. See `docs/ui/code-editor.md`.
- Added a built-in XML `SyntaxDefinition` (available as `SyntaxDefinition::getBuiltIn ("xml")` and matched for `.xml`, `.svg`, `.html`, `.xaml` and other markup extensions), with `<!-- -->` block comments, tag/attribute punctuation and `<? ?>` / `<!` / `</` / `/>` operator highlighting. The `CodeEditor` demo now has a language dropdown to switch between the built-in C++ / GLSL / Python / XML definitions.
- Fixed `CodeDocument`: `newLineChars` was default-constructed to an empty string instead of `"\n"`, breaking `getText()`, `getTextInRange()`, and character-offset calculations for all multi-line documents; `applyEdit()` returned a wrong caret column for single-line insertions (omitted `startIndex`), making every subsequent undo call operate on an inverted range and silently no-op; removed the `endsWithNewline` special case that returned a pre-newline position and similarly broke undo for Enter at the beginning of a line or in the middle of a line.
- Fixed `CodeEditor`: `undo()` and `redo()` now clamp `caretPosition` to the new document length and clear the selection after each operation, preventing an out-of-bounds caret after undo shrinks the document; `replaceNext()` now uses the position returned by `replaceRange` instead of `selectionStart + replacement.length()`.
- Fixed `CodeTokeniser`: cutting or deleting text that removes one or more lines left the token cache larger than the document and the forward-propagation stability check could declare a line whose content had shifted "unchanged", returning stale tokens (wrong syntax colors) for every line below the cut point. `codeDocumentChanged` now shrinks the cache to the new document line count and proactively marks all shifted lines dirty before the stability pass runs. The same problem existed in the other direction and was more visible in practice: inserting a line (pressing Enter, or a multi-line paste) grew the document but `codeDocumentChanged` had no branch for it at all, so every cached entry at or after the edit point kept referring to whatever used to be at that index — one or more lines off from where it actually was — and the stability check could decide a shifted-in line's state was "unchanged" and never mark it dirty, leaving it with stale, wrongly-sized tokens that fail to tile the line and fall back to unhighlighted plain text. Both directions are now handled the same way: resize to the new line count and mark everything from the edit point to the new end dirty, so misaligned cache entries are always discarded and recomputed from the live document text rather than reused.
- Fixed `CodeDocument::setText()` freezing for several seconds on a large paste: its line-splitting helper indexed the (UTF-8-backed) input `String` by character position inside the split loop, and both `operator[]` and `length()` are O(n) for UTF-8, turning the split into O(n²). It now walks the text once with a `CharPointer`.
- Fixed `CodeEditor` drawing selected/highlighted/caret text past the gutter and minimap when scrolled horizontally, since nothing clipped that content to the text area; the gutter's painted background also stopped 4px short of where the text area actually starts (disagreeing with the hit-test boundary used by `mouseDown()`), reading as misalignment on the left. The minimap overview now merges lines that map to less than one device pixel row into a single bar instead of issuing one `fillRect()` per source line on every paint regardless of visibility.
- Fixed `StyledText::update()` calling `Font::getPath()` (a CoreText round-trip on Apple platforms) once per glyph *occurrence* instead of once per unique glyph; a glyph's outline is the same every time for a given font, so it's now cached and reused, cutting a measured 240ms of the 453ms spent reshaping text on a single keystroke.
- Fixed `CodeEditor` reshaping (tokenizing, laying out and re-tessellating) the entire document on every single edit, making typing in a large file cost seconds per keystroke (measured: 2s for one backspace in a large file, mostly `StyledText::update()`). `styledText` now only ever holds the currently visible lines rather than the whole document; scrolling reshapes just the newly-visible range. Selection, search highlights, the caret, and Up/Down arrow navigation were adjusted to work correctly when their target is outside the currently-shaped range (falling back to an exact document-position computation rather than depending on `styledText`). Components that never call `setSize()`/`setBounds()` on their `CodeEditor` (as none of its unit tests do) keep shaping the whole document, since there's no meaningful "visible range" to restrict to without a real size.


### Shading
Expand Down Expand Up @@ -151,6 +169,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Bug Fixes

- `StyledText` caret bounds, hit-testing and selection rectangles now use line-relative glyph x positions computed with the same accumulation as drawing, instead of rive's paragraph-relative `GlyphRun::xpos`. Character positions were wrong on soft-wrapped lines (off by the width of all preceding text in the paragraph) and selection was drawn shifted on wrapped text; the caret at the first character of a wrapped line now lands on that line's left edge.
- iOS applications now use the `UIScene` lifecycle, removing UIKit's legacy lifecycle warning and ensuring SDL windows are created for the connected scene.
- Offscreen GPU rendering now supports recursive targets on Metal, OpenGL/GLES, and D3D11, so Lottie alpha/luma mattes, isolated-opacity layers, and cached precomps retain GPU compositing when rendered into an `Image` or `GpuCanvas`. Each `RenderableTarget` leases a Rive render context exclusively for its lifetime and returns it to the pool when destroyed. Repeated Lottie matte and precomp renders now reuse their canvases rather than allocating GPU textures each frame. Metal child targets allocate only their Rive render-canvas output texture; the CPU readback staging texture is created only when pixels are requested.
- Fixed undefined offscreen contents when nesting pooled render targets on all GPU backends. Render context slots were recycled whenever no frame was currently active, so two long-lived targets could share one slot; once their frames nested — which happens as Lottie matte and precomp layers cross their in/out points and the nesting order changes between frames — the inner target skipped `beginFrame` and was then flushed against the outer target's frame descriptor.
Expand Down
51 changes: 47 additions & 4 deletions docs/graphics/fonts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,40 @@ or variable-axis settings.
### Loading a font

```cpp
Font font;
font.loadFromFile (File ("/path/to/Font.otf"));
// or from a memory buffer:
font.loadFromData (fontBytes);
// Load a font from a file:
auto result = Font::loadFontFromFile (File ("/path/to/Font.otf"));
if (result.wasOk())
Font font = result.getValue();

// Or from a memory buffer:
auto fromData = Font::loadFontFromData (fontBytes);
```

### Loading system and first-available fonts

The loaders above (plus the ones below) are statics returning a
`ResultValue<Font>` — check `wasOk()` / `failed()` and read the value with
`getValue()`:

```cpp
// Load a font from a file.
auto font = Font::loadFontFromFile (File ("/path/to/Font.otf"));

// Load the first file that exists from a list of candidates.
auto fallback = Font::loadFontFromFirstAvailableFile ({
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
});

// Load the platform's default serif / monospace system text font
// (CoreText system UI fonts on Apple platforms, well-known font files elsewhere).
auto serif = Font::loadSerifSystemTextFont();
auto monospace = Font::loadMonospaceSystemTextFont();

if (auto result = Font::loadFontFromFile (file); result.wasOk())
auto font = result.getValue(); // use the loaded font
else
Logger::outputDebugString (result.getErrorMessage());
```

### Font metrics
Expand Down Expand Up @@ -90,6 +120,19 @@ auto bodyFont = font.withHeight (14.0f);
auto titleFont = font.withHeight (24.0f);
```

The theme also carries separate serif and monospace text fonts. The default
theme embeds Roboto Flex as the serif font and JetBrains Mono Variable as the
monospace font (when `YUP_EMBED_DEFAULT_THEME_TEXT_SERIF_FONT` /
`YUP_EMBED_DEFAULT_THEME_TEXT_MONOSPACE_FONT` are enabled, forced on
Emscripten), falling back to the platform system fonts otherwise:

```cpp
auto theme = ApplicationTheme::getGlobalTheme();
theme->setDefaultMonospaceFont (Font::loadMonospaceSystemTextFont().valueOr (theme->getDefaultFont()));

auto mono = theme->getDefaultMonospaceFont(); // e.g. the CodeEditor default
```

## StyledText

`StyledText` is a pre-laid-out, optionally rich text block - a batch built from
Expand Down
Loading
Loading