Added an option to set dynamic theming in the UICodeEditor.

This commit is contained in:
Martín Lucas Golini
2026-05-01 01:07:06 -03:00
parent fc45707cd1
commit 936938b71f
11 changed files with 101 additions and 168 deletions

View File

@@ -1,57 +0,0 @@
# UI Layout Phase 7: CSS List Style Type Plan
This document outlines the architectural plan for implementing the CSS `list-style-type` property within the decoupled layout system. This replaces the current CSS background-image hacks with proper text-based list markers.
**AGENT DIRECTIVE (CRITICAL):** You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase 7.X passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
---
## IMPLEMENTATION HAZARDS (READ BEFORE CODING)
1. **Property Inheritance:** `list-style-type` must be an inherited property so that setting it on `<ul>` or `<ol>` applies it to the `<li>` children.
2. **Clipping:** `UIRichText` clips its content to the content box (`mSize - mPadding`). In CSS, when `list-style-position` is `outside` (the default), the marker is drawn in the padding/margin area. The marker MUST be drawn outside of the `clipSmartEnable` block in `UIRichText::draw()` to remain visible.
3. **Sibling Indexing:** Ordered lists (`decimal`, etc.) require knowing the element's index. Only count previous siblings that are actual `<li>` elements (check `getTag() == "li"` or similar).
---
## Phase 7: List Style Type implementation
**Step 7.1: CSS Enums and Properties**
- In `csslayouttypes.hpp`, define:
```cpp
enum class CSSListStyleType { None, Disc, Circle, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman };
```
And its helper functions (`CSSListStyleTypeHelper::fromString`, etc.).
- In `propertydefinition.hpp`, add `PropertyId::ListStyleType`.
- In `stylesheetspecification.cpp`, register the property as inherited:
```cpp
registerProperty( "list-style-type", "none", true );
```
- In `UIHTMLWidget`, add `mListStyleType` defaulting to `None`. Update `applyProperty` to parse it.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
**Step 7.2: Marker String Generation & Text Cache**
- In `include/eepp/ui/uirichtext.hpp`, add a `Text mListMarkerText;` member to cache the rendered marker.
- Add a helper function `String getListMarkerString() const;` that:
- Returns `""` for `None`.
- Returns appropriate unicode characters for `Disc` ("•"), `Circle` ("○"), `Square` ("■").
- For ordered types like `Decimal`, counts preceding `<li>` siblings and formats the string (e.g., `1. `, `2. `).
- In `UIRichText::updateLayout()`, after styling is resolved, update `mListMarkerText`:
- Set its string using `getListMarkerString()`.
- Copy the font, size, and color from `mRichText.getFontStyleConfig()`.
- **Validation:** Compile and run all tests. (Snapshot)
**Step 7.3: Rendering the Marker**
- In `UIRichText::draw()`, add logic to render `mListMarkerText` if its string is not empty.
- **Positioning:**
- `X`: `mScreenPos.x + mPaddingPx.Left - mListMarkerText.getTextWidth() - offset`. (You may use a small hardcoded offset like `0.25em` derived from the font size to give it breathing room from the text).
- `Y`: `mScreenPos.y + mPaddingPx.Top`. This aligns it with the start of the first line of text.
- Ensure the drawing logic is placed *outside* the `if (isClipped()) { clipSmartEnable... }` block so it is not clipped away by the padding.
- **Validation:** Compile and run all tests. (Snapshot)
**Step 7.4: Remove CSS Hacks & Update Tests**
- Open `bin/assets/ui/breeze.css` and remove the `background-image`, `background-tint`, `background-position`, and `background-size` hacks for `ol > li` and `ul > li`.
- Change them to properly use `list-style-type`:
- `ul > li { list-style-type: disc; padding-left: 2em; }`
- `ol > li { list-style-type: decimal; padding-left: 2em; }`
- Create a specific unit test in `src/tests/unit_tests/uihtml_tests.cpp` (or a dedicated layout test) to verify `list-style-type: decimal` correctly increments numbers and `list-style-type: disc` draws a bullet.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)

View File

@@ -1,98 +0,0 @@
# UI Layout Separation & CSS Display/Position Support Plan (Strict Implementation Guide)
This document outlines the architectural plan for decoupling layout logic from specific widgets (like `UIRichText` and `UIHTMLTable`), introducing a robust generic layouter system, and supporting standard CSS `display` and `position` properties.
**AGENT DIRECTIVE (CRITICAL):** You are Negen. Fulfill this plan iteratively. You MUST compile and run the unit tests (`bin/unit_tests/eepp-unit_tests-debug`) after EVERY step. Do NOT proceed to the next step if there is even a 1-pixel difference in visual layout tests. Take a git stash snapshot (`git stash push -m "Phase X.Y passed" && git stash apply`) upon passing a step to keep a checkpoint while continuing to work. **If you need to restore a stash, use `git stash apply` instead of `git stash pop` so the stable snapshot is never lost.**
---
## IMPLEMENTATION HAZARDS (READ BEFORE CODING)
When migrating logic from Widgets to Layouters, you will face these traps:
1. **Pixel vs DP APIs:** Widgets have `getSize()` and `getPixelsSize()`, `getPadding()` and `getPixelsPadding()`. **Layouters MUST exclusively use the `Pixels` variants** (`getPixelsSize()`, `getPixelsPadding()`, `getLayoutPixelsMargin()`). Using the non-pixel variants will cause massive visual regressions due to DPI scaling.
2. **Infinite Recursion:** When a Layouter delegates to a widget's intrinsic width calculation, or vice versa, you must strictly manage the dirty flags (e.g., `mIntrinsicWidthsDirty = false`). Failure to clear these flags inside `computeIntrinsicWidths` will cause stack overflows.
3. **Table Hierarchy:** In HTML/eepp tables, the hierarchy is `Table` -> `TableSection` (`thead`, `tbody`) -> `TableRow` (`tr`) -> `TableCell` (`td`). The original code often checks `row->getParent()->isType(...)`. If you change sections to `UIHTMLWidget`, be extremely careful not to accidentally assign them a `BlockLayouter` that overrides the `TableLayouter`'s positioning.
---
## Phase 1: Core Infrastructure
**Step 1.1: CSS Properties & Enums**
- Add `Display`, `Position`, `Top`, `Right`, `Bottom`, `Left`, `ZIndex` to `PropertyId` enum (`propertydefinition.hpp`).
- Create `CSSDisplay` and `CSSPosition` enums (`csslayouttypes.hpp`).
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
**Step 1.2: Layouter Interfaces & Manager**
- Create `UILayouter` base interface (`uilayouter.hpp`). Must hold `UIWidget* mContainer` and `bool mValid`.
- Create `UILayouterManager` (`uilayoutermanager.hpp`/`cpp`) to spawn layouters.
- Create empty skeletons for `BlockLayouter`, `InlineLayouter`, `TableLayouter`, `NoneLayouter`.
- **CRITICAL:** Run `premake4` to regenerate makefiles now that new files are added.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
---
## Phase 2: UIHTMLWidget Base Class
**Step 2.1: Implement UIHTMLWidget**
- Create `UIHTMLWidget` inheriting from `UILayout`.
- Add CSS properties (`mDisplay`, `mPosition`, offsets, `mZIndex`).
- Implement `getLayouter()` which lazily instantiates via `UILayouterManager`.
- Override `onChildCountChange` and `onDisplayChange` to call `if (mLayouter) mLayouter->invalidate()`.
- **Validation:** Compile and run all tests. Must pass. (Snapshot)
---
## Phase 3: UIRichText & BlockLayouter (High Risk)
**Step 3.1: Inheritance and Access Control**
- Change `UIRichText` and `UITextSpan` to inherit from `UIHTMLWidget`.
- Add `BlockLayouter` and `InlineLayouter` as `friend class` to `UIRichText`, `UILayout`, and `UIWidget`.
- **CRITICAL:** In `UIHTMLWidget::getLayouter()`, temporarily return `nullptr`. We want `UIRichText` to still use its monolithic logic while we set up inheritance.
- **Validation:** Compile and run `UIRichText.*` tests. Must pass exactly. (Snapshot)
**Step 3.2: Implement BlockLayouter**
- Copy `UIRichText::updateLayout()`, `getMinIntrinsicWidth()`, and `getMaxIntrinsicWidth()` into `BlockLayouter`.
- **INVARIANTS TO MAINTAIN:**
- You MUST use `getPixelsPadding()` everywhere `mPaddingPx` was used.
- You MUST use `getPixelsSize()` everywhere `mSize` was used.
- The `mResizedCount` loop must be preserved: if `richText->mResizedCount > 0` after `setInternalPixelsWidth/Height`, `positionRichTextChildren` must run again.
- Do NOT skip `MatchParent` children in the `positionRichTextChildren` while loop.
- Enable `BlockLayouter` for `UIRichText` (but explicitly disable it for `UI_TYPE_HTML_TABLE_CELL` for now to isolate bugs).
- **Validation:** Compile and run `UIRichText.*` tests. **Zero pixel difference allowed.** (Snapshot)
**Step 3.3: Implement InlineLayouter**
- Replicate inline logic for `UITextSpan` into `InlineLayouter`.
- **Validation:** Compile and run all tests. (Snapshot)
---
## Phase 4: UIHTMLTable Refactoring (High Risk)
**Step 4.1: Table Base Classes & Friends**
- Change `UIHTMLTable`, `UIHTMLTableRow`, `UIHTMLTableHead`, `UIHTMLTableBody`, `UIHTMLTableFooter` to inherit from `UIHTMLWidget`.
- Make `TableLayouter` a friend of `UIHTMLTable`.
- **CRITICAL:** `UILayouterManager` MUST return `nullptr` for Table Sections and Table Rows. Only the `Table` itself gets `TableLayouter`.
- **Validation:** Compile and run `UIHTMLTable.*` tests. (Snapshot)
**Step 4.2: Implement TableLayouter**
- Move `UIHTMLTable::updateLayout()` and `computeIntrinsicWidths()` into `TableLayouter`.
- **INVARIANTS TO MAINTAIN:**
- `computeIntrinsicWidths` MUST clear `table->mIntrinsicWidthsDirty = false;` at all exit points to prevent recursion.
- `currentY` for rows MUST be accumulated exactly as: `Float currentY = padding.Top + mCellspacing - headHeight;` then incremented by `rowHeight + mCellspacing`. Do not attempt to "fix" this math; it offsets based on section anchors.
- When determining `headHeight`, use `row->getParent()->isType(UI_TYPE_HTML_TABLE_HEAD)`. Do not check the cell's parent.
- Enable `TableLayouter` in `getLayouter()`.
- **Validation:** Compile and run `UIHTMLTable.*` tests. **Zero pixel difference allowed.** (Snapshot)
**Step 4.3: Unify TableCell with BlockLayouter**
- Now that TableLayouter is proven, allow `UILayouterManager` to return `BlockLayouter` for `CSSDisplay::TableCell`.
- Ensure `BlockLayouter` does NOT override fixed widths if the container is a `TableCell` (the table layouter owns the cell width).
- **Validation:** Run all tests. (Snapshot)
---
## Phase 5: CSS Position (Out-of-Flow)
**Step 5.1: Position Implementation**
- Implement `getContainingBlock()` in `UIHTMLWidget`.
- Update layouters to skip children with `position: absolute|fixed`.
- Add `positionOutOfFlowChildren()` to `UIHTMLWidget::updateLayout()` after the layouter finishes.
- **Validation:** Compile and run all tests. Existing UI should remain unaffected. (Snapshot)

View File

@@ -81,6 +81,7 @@ em {
font-style: italic;
}
CodeEditor,
code {
font-family: monospace;
}
@@ -224,13 +225,11 @@ MarkdownView table > thead > tr > th {
}
MarkdownView CodeEditor,
MarkdownView code {
padding: 4dp;
}
MarkdownView code {
font-family: monospace;
background-color: var(--button-back);
font-size: 11dp;
padding: 4dp;
}
pushbutton,

View File

@@ -260,7 +260,9 @@ constexpr auto SyntaxStyleEmpty() {
*/
class EE_API SyntaxColorScheme {
public:
static SyntaxColorScheme getDefault();
static SyntaxColorScheme getDefaultDark();
static SyntaxColorScheme getDefaultLight();
static std::vector<SyntaxColorScheme> loadFromStream( IOStream& stream );

View File

@@ -852,6 +852,10 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
void setUseDefaultStyle( bool use );
bool dynamicTheming() const { return mDynamicTheming; }
void setDynamicTheming( bool set );
protected:
struct LastXOffset {
TextPosition position{ 0, 0 };
@@ -901,6 +905,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
bool mTabStops{ false };
bool mKerningEnabled{ false };
bool mDisableScrollInvalidation{ false };
bool mDynamicTheming{ false };
DocumentView mDocView;
Clock mBlinkTimer;
Time mBlinkTime;
@@ -1229,6 +1234,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
virtual void onClassChange();
inline bool needsHorizontalLength() const;
void updateDynamicTheme();
};
}} // namespace EE::UI

View File

@@ -41,8 +41,9 @@ KeyframesDefinition::getPropertyDefinitionList() const {
std::map<PropertyId, const PropertyDefinition*> propDefs;
for ( auto& block : keyframeBlocks ) {
for ( auto& property : block.second.properties ) {
propDefs[property.second.getPropertyDefinition()->getPropertyId()] =
property.second.getPropertyDefinition();
auto propDef = property.second.getPropertyDefinition();
if ( propDef )
propDefs[propDef->getPropertyId()] = property.second.getPropertyDefinition();
}
}
return propDefs;

View File

@@ -37,7 +37,7 @@ namespace EE { namespace UI { namespace Doc {
// "minimap_highlight" (Minimap text highlight color)
// "minimap_visible_area" (Minimap visible area marker color)
SyntaxColorScheme SyntaxColorScheme::getDefault() {
SyntaxColorScheme SyntaxColorScheme::getDefaultDark() {
return {
"eepp",
{
@@ -86,6 +86,55 @@ SyntaxColorScheme SyntaxColorScheme::getDefault() {
{ "minimap_visible_area"_sst, Color( "#FFFFFF0A" ) } } };
}
SyntaxColorScheme SyntaxColorScheme::getDefaultLight() {
return {
"github",
{
{ "normal"_sst, Color( "#24292e" ) },
{ "symbol"_sst, Color( "#24292e" ) },
{ "comment"_sst, Color( "#6a737d" ) },
{ "keyword"_sst, Color( "#d73a49" ) },
{ "type"_sst, Color( "#d73a49" ) },
{ "parameter"_sst, Color( "#005cc5" ) },
{ "number"_sst, Color( "#005cc5" ) },
{ "literal"_sst, Color( "#005cc5" ) },
{ "string"_sst, Color( "#032f62" ) },
{ "operator"_sst, Color( "#d73a49" ) },
{ "function"_sst, Color( "#005cc5" ) },
{ "link"_sst, Color( "#0366d6" ) }, // Using 'accent' for link color
{ "link_hover"_sst, { Color::Transparent, Color::Transparent, Text::Underlined } },
},
{ { "background"_sst, Color( "#fbfbfb" ) },
{ "widget_background"_sst, Color( "#f6f6f6" ) },
{ "text"_sst, Color( "#404040" ) },
{ "caret"_sst, Color( "#181818" ) },
{ "selection"_sst, Color( "#b7dce8" ) },
{ "line_highlight"_sst, Color( "#f2f2f2" ) },
{ "line_number"_sst, Color( "#d0d0d0" ) },
{ "line_number2"_sst, Color( "#808080" ) },
// eepp colors
{ "gutter_background"_sst, Color( "#fbfbfb" ) },
{ "whitespace"_sst, Color( "#b7dce8" ) },
{ "line_break_column"_sst, Color( "#d0d0d099" ) },
{ "matching_bracket"_sst, Color( "#00000033" ) }, // Dark transparent for light theme
{ "matching_selection"_sst, Color( "#a1c8d6" ) },
{ "matching_search"_sst, Color( "#e8e8e8" ) },
{ "suggestion"_sst, { Color( "#1d1f27" ), Color( "#e1e1e6" ), Text::Regular } },
{ "suggestion_scrollbar"_sst, { Color( "#3daee9" ) } },
{ "suggestion_selected"_sst, { Color( "#222533" ), Color( "#ffffff" ), Text::Regular } },
{ "error"_sst, { Color( "#990000FF" ) } },
{ "warning"_sst, { Color( "#999900FF" ) } },
{ "notice"_sst, Color( "#8abdff" ) },
{ "selection_region"_sst, Color( "#b7dce877" ) },
// minimap colors
{ "minimap_background"_sst, Color( "#fbfbfbAA" ) },
{ "minimap_current_line"_sst, Color( "#0000000A" ) },
{ "minimap_hover"_sst, Color( "#00000010" ) },
{ "minimap_selection"_sst, Color( "#b7dce880" ) },
{ "minimap_highlight"_sst, Color( "#FF00FFFF" ) },
{ "minimap_visible_area"_sst, Color( "#00000011" ) } } };
}
SyntaxColorScheme::Style parseStyle(
const std::string& value, bool* colorWasSet = nullptr,
const UnorderedMap<SyntaxStyleType, SyntaxColorScheme::Style>* syntaxColors = nullptr ) {
@@ -151,7 +200,7 @@ SyntaxColorScheme::Style parseStyle(
std::vector<SyntaxColorScheme> SyntaxColorScheme::loadFromStream( IOStream& stream ) {
Clock clock;
std::vector<SyntaxColorScheme> colorSchemes;
SyntaxColorScheme refColorScheme( getDefault() );
SyntaxColorScheme refColorScheme( getDefaultDark() );
IniFile ini( stream );
for ( size_t keyIdx = 0; keyIdx < ini.getNumKeys(); keyIdx++ ) {
SyntaxColorScheme colorScheme;
@@ -216,7 +265,7 @@ SyntaxColorScheme::SyntaxColorScheme( const std::string& name,
mName( name ), mSyntaxColors( syntaxColors ), mEditorColors( editorColors ) {}
static const SyntaxColorScheme::Style StyleEmpty = { Color::Transparent };
static const SyntaxColorScheme StyleDefault = SyntaxColorScheme::getDefault();
static const SyntaxColorScheme StyleDefault = SyntaxColorScheme::getDefaultDark();
const SyntaxColorScheme::Style&
SyntaxColorScheme::getSyntaxStyle( const SyntaxStyleType& type ) const {

View File

@@ -105,7 +105,7 @@ UICodeEditorSplitter::UICodeEditorSplitter( UICodeEditorSplitter::Client* client
? initColorScheme
: colorSchemes[0].getName();
} else {
mColorSchemes["default"] = SyntaxColorScheme::getDefault();
mColorSchemes["default"] = SyntaxColorScheme::getDefaultDark();
mCurrentColorScheme = "default";
}
}

View File

@@ -1,3 +1,4 @@
#include "eepp/ui/uistyle.hpp"
#include <algorithm>
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/fonttruetype.hpp>
@@ -155,7 +156,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis
mHeightPolicy = SizePolicy::Fixed;
mFlags |= UI_TAB_STOP | UI_OWNS_CHILDREN_POSITION | UI_SCROLLABLE;
setTextSelection( true );
setColorScheme( SyntaxColorScheme::getDefault() );
setColorScheme( SyntaxColorScheme::getDefaultDark() );
refreshTag();
mDocView.setOnVisibleLineCountChange( [this] {
onAutoSize();
@@ -3059,6 +3060,11 @@ bool UICodeEditor::applyProperty( const StyleSheetProperty& attribute ) {
setSyntaxDefinition(
SyntaxDefinitionManager::instance()->findFromString( attribute.asString() ) );
break;
case PropertyId::BackgroundColor: {
setBackgroundColor( attribute.asColor() );
updateDynamicTheme();
break;
}
default:
return UIWidget::applyProperty( attribute );
}
@@ -5772,4 +5778,27 @@ void UICodeEditor::setUseDefaultStyle( bool use ) {
invalidateDraw();
}
void UICodeEditor::setDynamicTheming( bool set ) {
if ( mDynamicTheming != set ) {
mDynamicTheming = set;
updateDynamicTheme();
invalidateDraw();
}
}
void UICodeEditor::updateDynamicTheme() {
if ( !mDynamicTheming )
return;
Color color( getFontColor() );
if ( mStyle && mStyle->getProperty( PropertyId::BackgroundColor ) )
color = getBackgroundColor();
if ( color != Color::Transparent ) {
if ( color.perceivedLuminance() > 128 ) {
setColorScheme( SyntaxColorScheme::getDefaultLight() );
} else {
setColorScheme( SyntaxColorScheme::getDefaultDark() );
}
}
}
}} // namespace EE::UI

View File

@@ -508,6 +508,7 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
UICodeEditor* editor = UICodeEditor::NewWithTag( "code" );
if ( editor ) {
editor->setParent( this );
editor->setDynamicTheming( true );
editor->loadFromXmlNode( child );
editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
editor->setLineWrapMode( LineWrapMode::Word );

View File

@@ -1006,7 +1006,7 @@ UTEST( FontRendering, TextHardWrap ) {
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
auto colors = SyntaxColorScheme::getDefault();
auto colors = SyntaxColorScheme::getDefaultDark();
auto syntax = SyntaxDefinitionManager::instance()->getByLanguageName( "Markdown" );
app.getWindow()->setClearColor( colors.getEditorColor( "background"_sst ).toRGB() );