diff --git a/doc/dynamic-glyph-redefinition.md b/doc/dynamic-glyph-redefinition.md new file mode 100644 index 0000000000..07a8b25eac --- /dev/null +++ b/doc/dynamic-glyph-redefinition.md @@ -0,0 +1,91 @@ +# OSC glyph: Dynamic Glyph Redefinition + +The `OSC glyph` sequence allows an application to redefine the visual representation of a specific **grapheme cluster** by injecting SVG data directly into the terminal's glyph atlas. This mechanism enables custom icons, complex widgets, and high-fidelity script rendering within the standard text stream. + +## Syntax + +`OSC` `glyph` `;` `` `;` `` `;` `` `ST` + +### Parameters + +| Parameter | Description | +| :--- | :--- | +| **`mappings`** | A comma-separated list of `cluster=id` assignments. The `cluster` is the UTF-8 sequence (including `STX` and `U+Dxxxx` boundaries). The `id` refers to the `id` attribute within the SVG. If no `=` is present, the entire SVG is mapped. | +| **`metrics`** | A reference character (e.g., `M`) used for bounding box/alignment. If **empty**, the glyph fills the character matrix area defined by the cluster (VT2D dimensions). | +| **`svg_payload`**| Raw XML/SVG data. The parser consumes all bytes after the third semicolon until the **ST** is encountered. | + +--- + +## Visual Inheritance & Layering + +The terminal follows a layered rendering model, treating custom glyphs as dynamic font characters: + +1. **Background Layer**: The cell matrix is pre-filled with the current **Background Color** (SGR). +2. **Glyph Layer**: The SVG is rendered on top. + - **Transparency**: Uncovered areas reveal the pre-filled background. + - **Dynamic Color (`currentColor`)**: The `currentColor` keyword can be used in any attribute (fill, stroke, stop-color, etc.) to inherit the current **Foreground Color** (SGR). + - **Raster Integration**: The SVG may contain `` tags with **Base64-encoded** raster data (Data URIs). External URLs are prohibited. + +--- + +## Interaction with VT2D (Character Geometry) + +1. **Manual Boundaries**: When using `STX ... U+Dxxxx`, the entire sequence acts as the unique lookup key in the atlas. +2. **Geometry Inheritance**: The SVG scales to the matrix dimensions specified by the `U+Dxxxx` codepoint. +3. **Fallback**: If rendering fails, the terminal displays the raw text characters within the cluster. + +--- + +## Examples + +### 1. Simple Emoji Replacement + +Override the standard 🚀 emoji using the proportions of an 'M' character. +```bash +\x1b]glyph;🚀;M;...\x1b\\ +``` + +### 2. Multi-Glyph Atlas (OT-SVG Style) + +Map different IDs from a single SVG document to specific manual clusters. This is highly efficient for loading entire icon sets or complex UI kits in a single atomic operation. +```bash +# Mapping 'cpu' to #icon1 and 'ram' to #icon2 +\x1b]glyph;\x02cpu\uD009C=icon1,\x02ram\uD009C=icon2;;......\x1b\\ +``` + +### 3. Full-Cell Widget + +Define a 3-cell wide area using VT2D manual clustering and fill it with an SVG sparkline. By leaving the `` field empty, the SVG is instructed to stretch and fill the entire available character matrix area. + +```bash +\x1b]glyph;\x02chart\uD009F;;...\x1b\\ +``` + +### 4. Advanced Styling Example (Theming & Gradients) + +This example uses `currentColor` to match the terminal's text color and an overlay gradient for a "shimmer" or "fade" effect. + +```svg + + + + + + + + + + + + +``` + +--- + +## Implementation Notes + +- **Caching**: The terminal caches the **SVG DOM** for efficiency. If the cell size (font size) changes, the terminal re-rasterizes the existing DOM without re-parsing the XML. +- **Persistence**: Mappings affect all current and future occurrences of the cluster in the session. +- **Clearing**: To remove a mapping, send an empty ``. To reset the atlas, send empty ``. +- **Resource Management**: Implementations should support a payload limit of **10MB to 40MB** to accommodate large icon sets or embedded rasters. +- **Parser Logic**: All data after the third semicolon is treated as a raw byte stream until the **ST** sequence (`ESC \` or `BEL`). diff --git a/src/netxs/desktopio/application.hpp b/src/netxs/desktopio/application.hpp index 14c120a41c..8d31252376 100644 --- a/src/netxs/desktopio/application.hpp +++ b/src/netxs/desktopio/application.hpp @@ -22,7 +22,7 @@ namespace netxs::app namespace netxs::app::shared { - static const auto version = "v2026.03.26"; + static const auto version = "v2026.03.27"; static const auto repository = "https://github.com/directvt/vtm"; static const auto usr_config = "~/.config/vtm/settings.xml"s; static const auto sys_config = "/etc/vtm/settings.xml"s; diff --git a/src/netxs/desktopio/canvas.hpp b/src/netxs/desktopio/canvas.hpp index d4efec2335..9351c84890 100644 --- a/src/netxs/desktopio/canvas.hpp +++ b/src/netxs/desktopio/canvas.hpp @@ -1052,11 +1052,11 @@ namespace netxs a = factor + a * inv_factor; return *this; } - // irgb: Pack fgc_alpha into exponent/mantissa. Use the range [2.0, 4.0), where the exponent is always 0x40. - void pack_alpha(byte alpha) // Bits: [Sign: 0][Exponent: 10000000][Mantissa: alpha_bits(7) + fgc_alpha(8) + padding(8)] + // irgb: Pack extra alpha into exponent/mantissa. + void pack_alpha(byte extra_alpha) { - auto a_bits = std::bit_cast(a); - auto packed = 0x40000000u | (alpha << 8) | (a_bits & 0xFF); // Take the upper bits of the mantissa alpha (for precision) and insert alpha. + auto a_8bit = (ui32)(a * 255.f + 0.5f); + auto packed = 0x40000000u | (extra_alpha << 15) | (a_8bit << 7); // extra_alpha: bits 15-22, a_8bit: bits 7-14. a = std::bit_cast(packed); } // irgb: Return true if alpha channel has extra alpha value. @@ -1068,26 +1068,22 @@ namespace netxs // irgb: Unpack and restore pure alpha. Return extra alpha value and normalize the current alpha channel. byte unpack_alpha() { - auto a_bits = std::bit_cast(a); - auto alpha = (byte)((a_bits >> 8) & 0xFF); - auto pure_bits = (ui32)(a_bits & 0xFF) << 15; // Return the mantissa bits to their place. - a = std::bit_cast(pure_bits | 0x3F000000u) - 0.5f; // Hacks for accuracy. - // a = (fp32)(a_bits & 0xFF) / 255.0f; // Or 8-bit accuracy. - return alpha; + auto extra = get_extra_alpha(); + restore_pure_alpha(); + return extra; } // irgb: Normalize the current alpha channel. void restore_pure_alpha() { auto a_bits = std::bit_cast(a); - auto pure_bits = (ui32)(a_bits & 0xFF) << 15; // Return the mantissa bits to their place. - a = std::bit_cast(pure_bits | 0x3F000000u) - 0.5f; // Hacks for accuracy. - // a = (fp32)(a_bits & 0xFF) / 255.0f; // Or 8-bit accuracy. + auto a_8bit = (a_bits >> 7) & 0xFF; + a = a_8bit * inv_255; } // irgb: Unpack and return an extra alpha value. byte get_extra_alpha() const { auto a_bits = std::bit_cast(a); - return (byte)((a_bits >> 8) & 0xFF); + return (byte)((a_bits >> 15) & 0xFF); } // irgb: PMA sRGB (8-bit) -> PMA Linear (irgb). static auto pma_srgb_to_pma_linear(argb pma_pixel) requires(std::is_floating_point_v) diff --git a/src/netxs/desktopio/gui.hpp b/src/netxs/desktopio/gui.hpp index 4beddb87d1..fcf72fc4ec 100644 --- a/src/netxs/desktopio/gui.hpp +++ b/src/netxs/desktopio/gui.hpp @@ -188,7 +188,7 @@ namespace netxs::gui axis_rec_t weight_target{}; axis_rec_t italic_target{}; std::vector palette; // CPAL cached palette in rgb-linear space. - std::unordered_map> svg_cache; // Face specific SVG-document cache. + std::unordered_map, 3>> svg_cache; // Face specific SVG-document cache. We storing several documents for the currentColor workaround. auto get_weight_str() const { @@ -1893,6 +1893,57 @@ namespace netxs::gui } } } + void draw_svg_to_canvas(auto& canvas, + lunasvg::Bitmap const& bitmap_black, // currentColor = #000000 + lunasvg::Bitmap const& bitmap_trans, // currentColor = 0x00000000 + lunasvg::Bitmap const& bitmap_white, // currentColor = #FFFFFF + rect layer_area) + { + if (!bitmap_black.valid() || !bitmap_trans.valid() || !bitmap_white.valid()) return; + auto canvas_area = canvas.area(); + auto intersect = canvas_area.trim(layer_area); + if (!intersect) return; + auto dst_base = intersect.coor - canvas_area.coor; + auto src_base = intersect.coor - layer_area.coor; + auto src_stride_trans = bitmap_trans.stride() / sizeof(ui32); + auto src_stride_black = bitmap_black.stride() / sizeof(ui32); + auto src_stride_white = bitmap_white.stride() / sizeof(ui32); + auto src_data_trans = (ui32 const*)bitmap_trans.data() + src_base.y * src_stride_trans; + auto src_data_black = (ui32 const*)bitmap_black.data() + src_base.y * src_stride_black; + auto src_data_white = (ui32 const*)bitmap_white.data() + src_base.y * src_stride_white; + for (auto y = 0; y < intersect.size.y; ++y) + { + auto r_trans = src_data_trans + src_base.x; + auto r_black = src_data_black + src_base.x; + auto r_white = src_data_white + src_base.x; + for (auto x = 0; x < intersect.size.x; ++x) + { + auto white_px = argb{ r_white[x] }; + if (white_px.chan.a > 0) + { + auto dst_xy = twod{ dst_base.x + x, dst_base.y + y }; + auto& dst_px = canvas[dst_xy]; + if constexpr (std::is_same_v, irgb>) + { + auto trans_px = argb{ r_trans[x] }; + auto black_px = argb{ r_black[x] }; + auto alpha = (byte)std::max(0, (si32)white_px.chan.r - black_px.chan.r); + auto fgc_a = dst_px.has_extra_alpha() ? dst_px.unpack_alpha() : byte{}; + alpha = alpha + (255 - alpha) * fgc_a / 255; + dst_px.blend_pma(trans_px); + dst_px.pack_alpha(alpha); + } + else // Alpha-only canvas + { + dst_px = (byte)std::min(255, dst_px + white_px.chan.a); + } + } + } + src_data_trans += src_stride_trans; + src_data_black += src_stride_black; + src_data_white += src_stride_white; + } + } void resolveOTSVGVariables(std::span data) { auto crop = view{ data.data(), data.size() }; @@ -1905,7 +1956,7 @@ namespace netxs::gui auto comma = crop.find(',', pos); if (comma != view::npos && comma < end) { - data[pos] = data[pos+1] = data[pos+2] = data[pos+3] = ' '; // Wipe "var(". + data[pos] = data[pos + 1] = data[pos + 2] = data[pos + 3] = ' '; // Wipe "var(". for (auto i = pos + 4; i <= comma; ++i) // "--name," -> " ". { data[i] = ' '; @@ -2033,52 +2084,124 @@ namespace netxs::gui auto doc = (FT_SVG_Document)slot->other; if (doc->svg_document_length) { + //log(text{ (char const*)doc->svg_document, doc->svg_document_length }); //todo suggest that the lunasvg project do var(...) resolving on their side //todo suggest that the lunasvg project use custom allocators, this will solve memory allocation issues in one fell swoop - // Looking for the cached lunasvg::Document. + // Looking for the cached lunasvg::Documents. auto svg_doc_id = (doc->start_glyph_id << 16) | doc->end_glyph_id; auto& svg_cache = fs.bare_face_ptr->svg_cache; auto svg_doc_iter = svg_cache.find(svg_doc_id); if (svg_doc_iter == svg_cache.end()) { - auto svg_data = text{ (char*)doc->svg_document, doc->svg_document_length }; + auto svg_data = std::span{ (char*)doc->svg_document, doc->svg_document_length }; resolveOTSVGVariables(svg_data); - auto document = lunasvg::Document::loadFromData(svg_data.data(), svg_data.size()); - if (!document) + + // currentColor test + //auto test_view = view{ svg_data.data(), svg_data.size() }; + ////auto test_cc = utf::replace_all(test_view, "gold", "currentColor"); + //auto test_cc = utf::replace_all(test_view, "#C90900", "currentColor"); + //svg_data = test_cc; + + auto change_currentColor = [](std::span data, view colorString) -> auto& // Workaround for currentColor. + { + static thread_local auto matches = std::vector{}; // List of currentColors positions within the document. + matches.clear(); + if (colorString.size() <= "currentColor"sv.size()) + { + auto pos = ui64{}; + auto crop = view{ data.data(), data.size() }; + while((pos = crop.find("currentColor", pos)) != text::npos) + { + matches.push_back(pos); + std::copy(colorString.begin(), colorString.end(), data.begin() + pos); // "#556677" + auto end = pos + "currentColor"sv.size(); + pos += colorString.size(); + std::fill(data.begin() + pos, data.begin() + end, ' '); // Trailing spaces. + pos = end; + } + } + return matches; + }; + auto& matches = change_currentColor(svg_data, "#000000"); // Set black (default) for the currentColor if it is. + auto document_black = lunasvg::Document::loadFromData(svg_data.data(), svg_data.size()); + //auto document_black = uptr{}; + auto document_trans = uptr{}; + auto document_white = uptr{}; + if (!document_black) // Fallback to \uFFFD "�". { - static constexpr auto replacement_svg = R"==( - - )=="sv; - document = lunasvg::Document::loadFromData(replacement_svg.data(), replacement_svg.size()); // Fallback to \uFFFD "�". + static constexpr auto replacement_svg_0 = R"==( + + )=="sv; + auto black = utf::replace_all(replacement_svg_0, "currentColor", "#000000"); + document_black = lunasvg::Document::loadFromData(black.data(), black.size()); + auto trans = utf::replace_all(replacement_svg_0, "currentColor", "transparent"); + document_trans = lunasvg::Document::loadFromData(trans.data(), trans.size()); + auto white = utf::replace_all(replacement_svg_0, "currentColor", "#FFFFFF"); + document_white = lunasvg::Document::loadFromData(white.data(), white.size()); } - if (document) + else if (document_black && matches.size()) // We have currentColors. Generate addidtional document layers. { - svg_doc_iter = svg_cache.emplace(svg_doc_id, std::move(document)).first; + // "currentColor" + static constexpr auto pure_trans = "transparent "sv; + static constexpr auto pure_white = "#FFFFFF "sv; + for (auto pos : matches) + { + std::copy(pure_trans.begin(), pure_trans.end(), svg_data.begin() + pos); + } + document_trans = lunasvg::Document::loadFromData(svg_data.data(), svg_data.size()); + for (auto pos : matches) + { + std::copy(pure_white.begin(), pure_white.end(), svg_data.begin() + pos); + } + document_white = lunasvg::Document::loadFromData(svg_data.data(), svg_data.size()); } + svg_doc_iter = svg_cache.emplace(svg_doc_id, std::to_array({ std::move(document_black), + std::move(document_trans), + std::move(document_white) })).first; } if (svg_doc_iter != svg_cache.end()) + if (auto& [document_black_ptr, document_trans_ptr, document_white_ptr] = svg_doc_iter->second; document_black_ptr) { - auto& document = svg_doc_iter->second; static thread_local auto glyph_id = text{}; + static thread_local auto bitmaps = std::array(); glyph_id = "glyph" + std::to_string(glyph.index); // According to the OT-SVG standard, each glyph within a document must be contained within an element with id="glyph". - auto element = document->getElementById(glyph_id); - if (!element) // Render entire document If glyph not found. - { - element = document->documentElement(); - } auto w = glyph.b_box.size.x; auto h = glyph.b_box.size.y; - auto bounds = element.getBoundingBox().transform(element.getLocalMatrix()); - auto scale = std::min(w / bounds.w, h / bounds.h); - auto tx = (w - bounds.w * scale) / 2.f - bounds.x * scale; - auto ty = (h - bounds.h * scale) / 2.f - bounds.y * scale; - auto matrix = lunasvg::Matrix{ scale, 0, 0, scale, tx, ty }; - static thread_local auto bitmap = lunasvg::Bitmap{ w, h }; //todo unfy - if (bitmap.height() < h || bitmap.width() < w) bitmap = { w, h }; - else bitmap.clear(0); - element.render(bitmap, matrix); // Premultiplied ARGB32 pixel data. - draw_svg_to_canvas(canvas, bitmap, glyph.b_box); + auto render_bitmap = [&](auto& document_ptr, auto& bitmap) + { + auto& document = *document_ptr; + auto element = document.getElementById(glyph_id); + if (!element) // Render a whole document if glyph not found. + { + element = document.documentElement(); + } + auto bounds = element.getBoundingBox().transform(element.getLocalMatrix()); + auto scale = std::min(w / bounds.w, h / bounds.h); + auto tx = (w - bounds.w * scale) / 2.f - bounds.x * scale; + auto ty = (h - bounds.h * scale) / 2.f - bounds.y * scale; + auto matrix = lunasvg::Matrix{ scale, 0, 0, scale, tx, ty }; + if (bitmap.height() < h || bitmap.width() < w) + { + bitmap = { w, h }; + } + else + { + bitmap.clear(0); + } + element.render(bitmap, matrix); // Premultiplied ARGB32 pixel data. + }; + render_bitmap(document_black_ptr, bitmaps[0]); + if (document_trans_ptr) + { + render_bitmap(document_trans_ptr, bitmaps[1]); + render_bitmap(document_white_ptr, bitmaps[2]); + draw_svg_to_canvas(canvas, bitmaps[0], bitmaps[1], bitmaps[2], glyph.b_box); + } + else + { + draw_svg_to_canvas(canvas, bitmaps[0], glyph.b_box); + } } } }