Repository navigation
icy_engine/icy_parser_core: embedder and WASM API (optional fonts, active hyperlink, scroll dirty lines, parse pause) - #198
Merged
Conversation
The bitmap fonts under data/fonts are compiled in through font_bytes!/ font_str!, which expand to empty data when the new embedded-fonts feature (on by default) is off. The lookups then report those fonts as missing instead of panicking: from_ansi_font_page returns None, from_sauce_name returns Err, the Amiga fonts return None, and the font statics and BitFont::default() fall back to a blank 8x16 font. install_ansi_font(slot, font) fills an ANSI slot at run time, so an embedder that ships its own fonts can make CSI font selection work. The BGI stroke fonts stay embedded; RIP needs them and they are small.
The open OSC 8 link was pub(crate), so an embedder that tags cells with links as they are written (for its scrollback, say) couldn't see it. The getter returns the URI and the start position. Its doc notes that the finished links on the screen are screen-relative and don't move on scroll.
TextScreen::scroll_up and scroll_down changed every row of the scroll region without marking it dirty, so a renderer that redraws only the dirty lines missed CSI S/T and a line feed on the bottom row. Insert and delete line (CSI L, CSI M) had the same gap. All four now mark the lines they change. When the region spans the whole layer, scroll_up and scroll_down rotate the Line slice and blank the freed row instead of copying cell by cell. The line move is taken only when it gives the same result as the cell copy (no locked layer or alpha channel, no graphemes or sixels in the rows, every row a full line); otherwise the cell copy runs as before.
Foreground ANSI music and APC SyncTERM:A;Wait hold the stream until the audio is done, but parse() always consumes the whole buffer, so an embedder that feeds large chunks had to find those points itself. CommandParser::parse_until_pause works like parse() but returns the number of bytes consumed. AnsiParser asks CommandSink::should_pause right after play_music and aps, and stops there when it returns true; the caller passes the rest of the input later. Both new methods have defaults (never pause, consume everything), so parse() and existing sinks and parsers behave as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a few small APIs for embedding icy_engine outside icy_term. They come from enig-term, a browser terminal that compiles icy_engine to WASM and today carries local patches or workarounds for each of them. Every change is additive. The defaults keep icy_term's behaviour as it is, and each commit has its own test. These are your API decisions, so the choices are explained below and are easy to change.
Add an embedded-fonts feature and install_ansi_font to icy_engine
A new cargo feature,
embedded-fonts, is on by default. The font files underdata/fontsare now included through two crate-internal macros,font_bytes!andfont_str!. With the feature on they areinclude_bytes!andinclude_str!. With it off they expand to empty data.With the feature off, nothing panics and the lookups report the font as missing:
from_ansi_font_pagereturnsNone,from_sauce_namereturnsErr, and the Amiga font lookups returnNone;BitFont::default()fall back to a blank 8x16 font, or to the font installed in slot 0;ansi_v2's font map skips slots that have no font instead of unwrapping.fonts::install_ansi_font(slot, font)fills an ANSI slot at run time, for the font's own height (8, 14 or 16).from_ansi_font_page, and soCSI Ps1 ; Ps2 SP D, then finds it. It shares the existing lazyOnceLockcache, so a slot and height can be set once. With embedded fonts on it works only before that slot is first used.The BGI
.CHRstroke fonts stay embedded. RIP needs them, the host has no other way to supply them, and they are about 120 KB.With the feature off,
cargo build -p icy_engine --no-default-features --target wasm32-unknown-unknownbuilds. In enig-term this cuts the gzipped WASM from 1.93 MB to 1.60 MB, and it replaces our patch that shadowedinclude_bytes!crate-wide.Tests:
fonts::embedded_fonts_tests.lookups_without_embedded_fontsruns only with the feature off (cargo test -p icy_engine --no-default-features --lib embedded_fonts). It checks every SAUCE name and every slot at every height, the statics,create_screenfor the screen modes that load fonts, and installing a slot font, including the fallback to an installed font of another height.install_ansi_font_rejects_bad_slot_and_heightruns in both configurations.install_ansi_font_fills_a_slot_height_onceruns with the feature on: it fills a height that was never loaded, leaves the embedded heights alone, and refuses a second install or one for a height that is already loaded. With the feature off, 9 existing lib tests fail because they assert things about the real fonts (for example the C64 fonts' 8x8 size). They could be gated on the feature if CI ever builds without it.Add TerminalState::active_hyperlink
active_hyperlink()returns the URI and start position of the open OSC 8 link, orNone. An embedder that tags cells with links as they are written, for its own scrollback, needs this. The doc comment also notes that the finished links stored on the screen are screen-relative and do not move when the screen scrolls.Test:
test_active_osc8_hyperlink_is_readableintests/text_screen.rs.Mark scrolled lines dirty and move whole lines on full-width scrolls
TextScreen::scroll_upandscroll_downnow mark the scroll region dirty, and so doinsert_terminal_lineandremove_terminal_line(CSI L,CSI M) for the lines they move. Before,get_dirty_lines()returnedNoneafterCSI S,CSI T,CSI L,CSI Mor a line feed on the bottom row, so a renderer that redraws only the dirty lines missed the scroll.When the region spans the whole layer, they now rotate the
Lineslice and blank the freed row instead of copying cell by cell.Layer::scroll_full_width_rows(pub(crate)) takes this path only when the result is the same as the cell copy: the layer is not locked or hidden, the alpha channel is not locked, the rows have no graphemes or sixels, and every row is exactly one layer wide. Otherwise it returns false and the cell copy runs as before. In enig-term's scrolling benchmark, moving lines was about 9x faster.Tests, in
text_screen.rs'sdirty_line_tests:scrolling_marks_the_scroll_region_dirtyfails without the change, andtest_scroll_sequences_mark_the_scrolled_lines_dirty(intests/text_screen.rs) checksCSI S,CSI T,CSI L,CSI M, a line feed andESC Dthrough the parser;full_width_scroll_moves_lines_with_the_cell_copy_resultchecks that the line move is taken and that the rows and the blank attribute match the cell copy;full_width_scroll_keeps_every_attribute_like_the_cell_copyscrolls cells with differing colours, blink and RGB, with and without top/bottom margins, and compares the whole layer with a cell-by-cell copy;line_move_declines_what_the_cell_copy_handles_differentlychecks the fallback cases.Let the sink stop the ANSI parse after a blocking construct
Foreground ANSI music and
APC SyncTERM:A;Waithold the stream until the audio is done, butparse()consumes the whole buffer. There are two new provided methods:CommandSink::should_pause(&mut self) -> booldefaults tofalse.AnsiParsercalls it right afterplay_musicandaps;CommandParser::parse_until_pause(&mut self, input, sink) -> usizeworks likeparseand returns the bytes consumed. It stops after the construct when the sink returns true. The caller feeds&input[n..]later. The default implementation callsparseand returnsinput.len().parse()itself never pauses and never callsshould_pause, so existing callers, sinks and parsers behave as before. A music string or APC inside a CTerm macro does not pause, because the macro is parsed from stored bytes and not from the caller's input.I picked this shape because it is additive. Changing
play_musicorapsto return a value would break every sink. A flag checked after every event would add a virtual call per event for a case that only these two constructs need.Tests:
tests/ansi/pause.rs. One test pauses after the music and after the Wait APC, does not pause after another APC, and resumes to the same events. Another checks thatparse()ignores pause requests and gives the same event stream. A third uses a sink that pauses after every construct and feeds the rest each time, for several inputs (back to back constructs, one as the last byte, an ESC inside an APC string, an unfinished sequence at the end), and compares the events with a singleparse().Deferred
Cell size per font (issue draft 17)
The renderer takes the cell size from the screen (slot 0), and
font_for_renderdraws any slot's glyphs into that cell. A DCS-loaded 8x8 font, or an 8x14 font selected over 8x16, is drawn at the wrong size. There are two options:CsiFontSelection, if the slot's font infont_tablehas a height other thanfont_dimensions().height, setFontSelectionState::Failureand keep the current font.from_ansi_font_pagealready picks the variant for the cell height, so only DCS-loaded and host-installed fonts are affected. This is small, but it changes what icy_term does with a mismatched DCS font today, so it may want an opt-in flag onTerminalState.font_for_rendersites inbuffers_rendering.rs(and the GPU path in icy_engine_gui) would scale each glyph from its font's size to the cell size. This keeps every font usable, but it is a larger change to the hot rendering loops and the shaders.enig-term does the first one outside the engine today.
iCE colours applied at render time (issue draft 09)
ScreenSink::display_attributerewrites blink into a bright background when a character is written, soCSI ? 33 h/ldoes not change cells already on screen. A render-time version would:display_attribute;buffers_rendering.rs, and the shader inputs in icy_engine_gui), whenice_colorsis set, treat blink as background index + 8 for palette backgrounds below 8, and never blink;TextAttribute::as_u8and the savers take the mode from the screen at save time, which they mostly already do throughIceMode.The risk is in the files and tests that expect the rewritten attribute, such as
ice_rgb_background_cell_renders_foreground. It also needs a decision on howice_colors(terminal state) andIceMode(buffer) relate, so this seems better done by you or after we talk.API choices you may want to change
embedded-fonts, on by default, rather than a runtime switch. The macros also mean every font file is still named in the source, so nothing moves.install_ansi_fontis a process-wide install into the existingOnceLockcache. It is set once and is not per screen. A trait-based font provider consulted byget_ansi_fontandload_sauce_font, or a replaceable cache, would allow lazy loading and replacement, but the lookups return&'static BitFont, so that needs a cache redesign. There is no SAUCE-name install yet. An embedder can load those withBitFont::from_bytesandset_font.BitFont::default()returns the font installed in slot 0 at 16px, or a blank 8x16 font. A blank default never panics, but it draws nothing until the host supplies a font.active_hyperlink()returnsOption<(&str, Position)>. It could instead return a small struct or theHyperLinktype. The draft also asked whether the stored links should move on scroll. I only documented that they don't.TextScreen's non-unicode path and to full-width regions.scroll_left/scroll_rightand the Amiga and palette screens are unchanged.should_pause(&mut self), asked only afterplay_musicandaps. If you would rather the sink can stop after any event (a DCS font load, a sixel), the same check could move to every emit, at the cost of a call per event.AvatarParser,PcBoardParserand the other wrappers aroundAnsiParseruse the default and don't pause yet.