Add configurable OpenType font features to text rendering:

Support explicit liga, calt, clig, and dlig shaping features and include
  text hints in layout caching, measurement, drawing, and character
  position calculations.

  Propagate scene-wide font feature defaults through UI text producers,
  with granular widget overrides and recursive cache invalidation. Update
  UICodeEditor and UIConsole to use shaped positioning when font features
  are enabled, including with monospaced fonts.

  Add separate UI and editor font feature settings to ecode, persist them
  as comma-separated OpenType tags, and apply changes immediately. Add
  localized menu labels, reusable Text feature conversion helpers, and
  regression coverage for shaping, positioning, propagation, and parsing.
This commit is contained in:
Martín Lucas Golini
2026-08-15 02:21:11 -03:00
parent c1034fa87e
commit ffabb19d60
37 changed files with 882 additions and 101 deletions
+7
View File
@@ -145,6 +145,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath,
editor.horizontalScrollbar = ini.getValueB( "editor", "horizontal_scrollbar", true );
editor.openDocumentsInMainSplit =
ini.getValueB( "editor", "open_documents_in_main_split", false );
editor.fontFeatures =
Graphics::Text::fontFeaturesFromString( ini.getValue( "editor", "font_features" ) );
ui.fontSize = ini.getValue( "ui", "font_size", "11dp" );
ui.panelFontSize = ini.getValue( "ui", "panel_font_size", "11dp" );
ui.showSidePanel = ini.getValueB( "ui", "show_side_panel", true );
@@ -169,6 +171,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath,
FontTrueType::fontHintingFromString( ini.getValue( "ui", "font_hinting", "full" ) );
ui.fontAntialiasing = FontTrueType::fontAntialiasingFromString(
ini.getValue( "ui", "font_antialiasing", "grayscale" ) );
ui.fontFeatures =
Graphics::Text::fontFeaturesFromString( ini.getValue( "ui", "font_features" ) );
ui.editorFontInInputFields = ini.getValueB( "ui", "editor_font_in_input_fields", true );
doc.trimTrailingWhitespaces = ini.getValueB( "document", "trim_trailing_whitespaces", false );
@@ -350,6 +354,8 @@ void AppConfig::save( const std::vector<std::string>& recentFiles,
ini.setValueB( "editor", "vertical_scrollbar", editor.verticalScrollbar );
ini.setValueB( "editor", "horizontal_scrollbar", editor.horizontalScrollbar );
ini.setValueB( "editor", "open_documents_in_main_split", editor.openDocumentsInMainSplit );
ini.setValue( "editor", "font_features",
Graphics::Text::fontFeaturesToString( editor.fontFeatures ) );
ini.setValue( "editor", "font_size", editor.fontSize.toString() );
ini.setValue( "ui", "font_size", ui.fontSize.toString() );
@@ -374,6 +380,7 @@ void AppConfig::save( const std::vector<std::string>& recentFiles,
ini.setValue( "ui", "font_hinting", FontTrueType::fontHintingToString( ui.fontHinting ) );
ini.setValue( "ui", "font_antialiasing",
FontTrueType::fontAntialiasingToString( ui.fontAntialiasing ) );
ini.setValue( "ui", "font_features", Graphics::Text::fontFeaturesToString( ui.fontFeatures ) );
ini.setValue( "screenshots", "save_path", screenshot.savePath );
ini.setValue( "screenshots", "filename_pattern", screenshot.filenamePattern );
ini.setValue( "screenshots", "save_format", screenshot.saveFormat );
+2
View File
@@ -81,6 +81,7 @@ struct UIConfig {
std::string language;
FontHinting fontHinting{ FontHinting::Full };
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
Uint32 fontFeatures{ 0 };
};
struct WindowStateConfig {
@@ -107,6 +108,7 @@ struct CodeEditorConfig {
std::string colorScheme{ "ecode" };
StyleSheetLength fontSize{ 11, StyleSheetLength::Dp };
StyleSheetLength lineSpacing{ 0, StyleSheetLength::Dp };
Uint32 fontFeatures{ 0 };
bool showLineNumbers{ true };
bool showWhiteSpaces{ true };
bool showLineEndings{ false };
+2
View File
@@ -1981,6 +1981,7 @@ void App::setTheme( const std::string& path ) {
->add( theme );
mUISceneNode->setTheme( theme.get() );
mUISceneNode->setDefaultTextHints( mConfig.ui.fontFeatures );
mUISceneNode->getRoot()->addClass( "appbackground" );
@@ -2986,6 +2987,7 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) {
editor->setFoldDrawable( findIcon( "chevron-down", PixelDensity::dpToPxI( 12 ) ) );
editor->setFoldedDrawable( findIcon( "chevron-right", PixelDensity::dpToPxI( 12 ) ) );
editor->setTabStops( mConfig.doc.tabStops );
editor->setLigatureFeatures( mConfig.editor.fontFeatures );
editor->setEnableInlineColorBoxes( config.inlineColorBoxes );
doc.setAutoCloseBrackets( !mConfig.editor.autoCloseBrackets.empty() );
+62
View File
@@ -3316,6 +3316,64 @@ UIMenu* SettingsMenu::createFontAntiAliasingMenu() {
UIMenu* SettingsMenu::createFontsMenu() {
mFontsMenu = UIPopUpMenu::New();
const auto createFontFeaturesMenu = [this]( bool editorFeatures ) {
auto* menu = UIPopUpMenu::New();
const Uint32 features = editorFeatures ? mApp->getConfig().editor.fontFeatures
: mApp->getConfig().ui.fontFeatures;
menu->addCheckBox( i18n( "standard_ligatures", "Standard Ligatures (liga)" ),
features & TextHints::StandardLigatures )
->setTooltipText(
i18n( "standard_ligatures_desc",
"Typographic combinations such as fi, fl, and ffi, depending on the font." ) )
->setId( "liga" );
menu->addCheckBox( i18n( "contextual_alternates", "Contextual Alternates (calt)" ),
features & TextHints::ContextualAlternates )
->setTooltipText(
i18n( "contextual_alternates_desc",
"Context-dependent alternatives, including many programming ligatures." ) )
->setId( "calt" );
menu->addCheckBox( i18n( "contextual_ligatures", "Contextual Ligatures (clig)" ),
features & TextHints::ContextualLigatures )
->setTooltipText(
i18n( "contextual_ligatures_desc",
"Ligatures applied in specific contexts to improve readability." ) )
->setId( "clig" );
menu->addCheckBox( i18n( "discretionary_ligatures", "Discretionary Ligatures (dlig)" ),
features & TextHints::DiscretionaryLigatures )
->setTooltipText(
i18n( "discretionary_ligatures_desc",
"Optional decorative or stylistic ligatures provided by the font." ) )
->setId( "dlig" );
menu->on( Event::OnItemClicked, [this, editorFeatures]( const Event* event ) {
if ( !event->getNode()->isType( UI_TYPE_MENUCHECKBOX ) )
return;
auto* item = event->getNode()->asType<UIMenuCheckBox>();
const String& id = item->getId();
const Uint32 feature = id == "liga" ? TextHints::StandardLigatures
: id == "calt" ? TextHints::ContextualAlternates
: id == "clig" ? TextHints::ContextualLigatures
: id == "dlig" ? TextHints::DiscretionaryLigatures
: 0;
if ( feature == 0 )
return;
Uint32& features = editorFeatures ? mApp->getConfig().editor.fontFeatures
: mApp->getConfig().ui.fontFeatures;
if ( item->isActive() )
features |= feature;
else
features &= ~feature;
if ( editorFeatures ) {
mSplitter->forEachEditor( [features]( UICodeEditor* editor ) {
editor->setLigatureFeatures( features );
} );
} else {
mApp->getUISceneNode()->setDefaultTextHints( features );
}
} );
return menu;
};
auto* uiFontFeaturesMenu = createFontFeaturesMenu( false );
auto* editorFontFeaturesMenu = createFontFeaturesMenu( true );
mFontsMenu->addSubMenu( i18n( "ui_font_hint", "Font Hint" ), findIcon( "font-size" ),
createFontHintMenu() );
mFontsMenu->addSubMenu( i18n( "ui_font_antialiasing", "Font Anti-Aliasing" ),
@@ -3325,10 +3383,14 @@ UIMenu* SettingsMenu::createFontsMenu() {
mFontsMenu
->add( i18n( "ui_font_and_size_ellipsis", "UI Font & Size..." ), findIcon( "font-size" ) )
->setId( "sans-serif-font" );
mFontsMenu->addSubMenu( i18n( "ui_font_features", "UI Font Features" ), findIcon( "font-size" ),
uiFontFeaturesMenu );
mFontsMenu
->add( i18n( "editor_font_and_size_ellipsis", "Editor Font & Size..." ),
findIcon( "font-size" ) )
->setId( "editor-font" );
mFontsMenu->addSubMenu( i18n( "editor_font_features", "Editor Font Features" ),
findIcon( "font-size" ), editorFontFeaturesMenu );
mFontsMenu
->add( i18n( "terminal_font_and_size_ellipsis", "Terminal Font & Size..." ),
findIcon( "font-size" ) )