diff --git a/.agent/plans/diff.md b/.agent/plans/diff.md deleted file mode 100644 index 860e47929..000000000 --- a/.agent/plans/diff.md +++ /dev/null @@ -1,84 +0,0 @@ -# Plan: UIDiffView Implementation - -## 1. Overview and Objectives -The goal of this project is to implement `UIDiffView`, a new widget within the `eepp` UI framework that provides a rich visual representation of code differences. - -### Key Objectives: -- **Phase 1: Unified View (Single Panel):** Display a unified diff with custom line background colors (red for removed, green for added, specific styling for headers). -- **Phase 2: Proper Syntax Highlighting:** Use language-specific syntax highlighting (e.g., C++, Python) for the text, rather than treating the entire file as a `.diff` text. -- **Phase 3: Diff Generation:** Integrate `dtl.h` (or similar) to generate diff information dynamically when comparing two text documents or strings, in addition to parsing existing `.patch` or `.diff` files. -- **Phase 4: Split View (Two Panel):** Support an optional side-by-side split view. - -## 2. Architecture & Design - -### 2.1 Widget Hierarchy -- **`UIDiffView`** will be a custom composite widget, *not* a direct subclass of `UICodeEditor`. -- By composing `UICodeEditor`(s) internally, `UIDiffView` can seamlessly transition between a "Unified" single-editor mode and a "Side-by-Side" two-editor mode in the future. -- `UIDiffView` will manage an internal instance of `UICodeEditor` (for unified view) and attach a custom `UICodeEditorPlugin` to handle custom background drawing. - -### 2.2 Data Model & Diff Processing -We need a representation of diff data that abstracts away whether the diff was loaded from a file or generated on the fly. -- Create a struct `DiffLine`: - ```cpp - enum class DiffLineType { Added, Removed, Context, Header }; - struct DiffLine { - DiffLineType type; - String text; - // Original line numbers (for the gutter later) - Int64 oldLineNum; - Int64 newLineNum; - }; - ``` -- **Parsing:** If loaded from a `.patch`/`.diff` file, a parser will extract `DiffLine`s and determine the underlying file extension (e.g., from `+++ b/src/main.cpp` -> `.cpp`). -- **Generation (dtl.h):** If provided two strings or `TextDocument`s (Old vs New), we will use `dtl.h` to compute the differences and generate a unified list of `DiffLine`s. - -### 2.3 Syntax Highlighting Challenge -A standard syntax highlighter will fail if lines start with `+` or `-` because it breaks language grammar. -**Solution:** -- The internal `TextDocument` of the `UICodeEditor` will hold the *clean* text (without the leading `+` or `-`). -- The syntax highlighter will run normally, initialized with the detected base language (e.g., C++). -- The `+` and `-` indicators will be drawn visually in the gutter or injected via rendering hooks, rather than being part of the raw `TextDocument` string. Alternatively, if we keep `+` and `-` in the string, we might need a composite `SyntaxHighlighter` that delegates to the underlying language while skipping the first character. *Recommendation: Strip `+`/`-` from the document text, and draw them manually during rendering to preserve perfect syntax highlighting.* - -### 2.4 Custom Rendering (Backgrounds & Indicators) -We will leverage `UICodeEditorPlugin` to draw custom line backgrounds without modifying the core `UICodeEditor` drawing routine. -- Create `UIDiffEditorPlugin : public UICodeEditorPlugin`. -- Override `drawBeforeLineText`: - - Check the line index against the list of `DiffLine`s. - - If `DiffLineType::Added`, draw a greenish `Primitives::drawRectangle` across the editor's width. - - If `DiffLineType::Removed`, draw a reddish rectangle. - - If `DiffLineType::Header`, draw a bluish/gray rectangle. -- Override `drawGutter` (or similar) if we want to display dual line numbers (Old and New) or custom `+`/`-` icons. - -## 3. Step-by-Step Execution Plan - -### Step 1: Core Diff Parsing & Generation -1. Integrate `dtl.h` into `src/eepp/thirdparty/dtl/` (if not already present). -2. Create `DiffDocument` (or `DiffData`) utility class capable of: - - Parsing a unified diff string into a structured format. - - Generating a unified diff structure from two source strings using `dtl.h`. - - Identifying the target language extension from diff headers. - -### Step 2: Custom Rendering Plugin -1. Implement `UIDiffEditorPlugin` inheriting from `UICodeEditorPlugin`. -2. Implement background drawing in `drawBeforeLineText` based on line states provided by `DiffDocument`. -3. Test drawing performance. Ensure no memory/object allocations happen during the render loop (Negen mandate). - -### Step 3: `UIDiffView` Implementation (Unified View) -1. Create `UIDiffView` widget (`include/eepp/ui/tools/uidiffview.hpp` and `src/eepp/ui/tools/uidiffview.cpp`). -2. Instantiate a read-only `UICodeEditor` internally. -3. Apply the `UIDiffEditorPlugin` to the editor. -4. Implement `loadFromPatch(const std::string& patchText)` and `loadFromStrings(const std::string& oldText, const std::string& newText)`. -5. Set the syntax definition of the internal `TextDocument` based on the detected file extension. - -### Step 4: Gutter and Line Numbers (Refinement) -1. Hide the default line number gutter of `UICodeEditor` or override it. -2. Draw custom line numbers representing both Old and New file line numbers. - -### Step 5: Integration into ecode -1. Map the `.diff` and `.patch` extensions to open in `UIDiffView` instead of standard `UICodeEditor` inside `ecode`. -2. Add a command/shortcut to "Compare against saved version" or "Compare against Git HEAD" which generates a diff dynamically using the newly integrated `dtl.h` logic. - -## 4. Performance & Memory Considerations (Negen's Directives) -- The mapping of line index to `DiffLineType` must be fast (e.g., an `std::vector` indexed directly by `lineIndex`). -- Do not allocate strings or complex objects inside the `drawBeforeLineText` or `drawGutter` render loops. -- Avoid modifying the `UICodeEditor` document layout excessively on the fly. Build the unified document cleanly once. \ No newline at end of file diff --git a/.ecode/project_build.json b/.ecode/project_build.json index f10d306cd..674aff317 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -375,6 +375,12 @@ "command": "${project_root}/bin/eepp-ui-dropdownmodellist-debug", "name": "eepp-ui-dropdownmodellist-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "-c system --hn-dark", + "command": "${project_root}/bin/eepp-ui-html-debug", + "name": "eepp-ui-html-debug", + "working_dir": "${project_root}/bin" } ], "var": { @@ -598,6 +604,18 @@ "command": "${project_root}/bin/unit_tests/eepp-unit_tests-debug", "name": "eepp-unit_test-debug", "working_dir": "${project_root}/bin/" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-html-debug", + "name": "eepp-ui-html-debug", + "working_dir": "${project_root}/bin/" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-markdownview-debug", + "name": "eepp-ui-markdownview-debug", + "working_dir": "${project_root}/bin/" } ], "var": { diff --git a/.github/workflows/ecode-nightly.yml b/.github/workflows/ecode-nightly.yml index a66d10694..a059cda44 100644 --- a/.github/workflows/ecode-nightly.yml +++ b/.github/workflows/ecode-nightly.yml @@ -18,21 +18,19 @@ jobs: version: ${{ steps.tag.outputs.version }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Tag id: tag run: | echo "version=nightly" >> "$GITHUB_OUTPUT" - name: Update Tag - uses: richardsimko/update-tag@v1 - with: - tag_name: ${{ steps.tag.outputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git tag -f ${{ steps.tag.outputs.version }} + git push -f origin ${{ steps.tag.outputs.version }} - name: Create Release id: create_release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ steps.tag.outputs.version }} name: ecode ${{ steps.tag.outputs.version }} @@ -63,7 +61,7 @@ jobs: apt-get update apt-get install -y --no-install-recommends software-properties-common build-essential git ca-certificates - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive', set-safe-directory: true } - name: Set Environment Variables run: | @@ -99,7 +97,7 @@ jobs: run: | bash projects/linux/ecode/build.app.sh --with-static-cpp --version ${{ env.INSTALL_REF }} --arch ${{ matrix.config.arch }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -122,7 +120,7 @@ jobs: CXX: g++ steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Environment Variables run: | @@ -158,7 +156,7 @@ jobs: bash projects/scripts/patch_commit_number.sh bash projects/linux/ecode/build.app.sh --version ${{ env.INSTALL_REF }} --arch ${{ matrix.config.arch }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -182,7 +180,7 @@ jobs: CXX: g++ steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Environment Variables run: | @@ -212,7 +210,7 @@ jobs: bash projects/scripts/patch_commit_number.sh bash projects/mingw32/ecode/build.app.sh --version ${{ env.INSTALL_REF }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -232,7 +230,7 @@ jobs: runs-on: ${{ matrix.config.container }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Environment Variables run: | @@ -253,7 +251,7 @@ jobs: bash projects/scripts/patch_commit_number.sh bash projects/mingw32/ecode/build.app.sh --arch=arm64 --version ${{ env.INSTALL_REF }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -278,7 +276,7 @@ jobs: MACOS_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: System Information run: | @@ -293,7 +291,7 @@ jobs: echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - name: Install Dependencies run: | - brew install bash sdl2 create-dmg premake p7zip + brew install bash sdl2 create-dmg premake - name: Build run: | bash projects/scripts/patch_commit_number.sh @@ -313,7 +311,7 @@ jobs: DMG_NAME="ecode-macos-${{ env.INSTALL_REF }}-arm64.dmg" bash ./sign.sh "$DMG_NAME" - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -338,7 +336,7 @@ jobs: MACOS_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }} steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: System Information run: | @@ -377,7 +375,7 @@ jobs: DMG_NAME="ecode-macos-${{ env.INSTALL_REF }}-x86_64.dmg" bash ./sign.sh "$DMG_NAME" - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -391,7 +389,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Environment Variables run: | @@ -417,7 +415,7 @@ jobs: bash projects/scripts/patch_commit_number.sh sh projects/freebsd/ecode/build.app.sh --version ${{ env.INSTALL_REF }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -431,7 +429,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Set Environment Variables run: | @@ -452,7 +450,7 @@ jobs: bash projects/scripts/patch_commit_number.sh sh projects/haiku/ecode/build.app.sh --version ${{ env.INSTALL_REF }} - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -472,7 +470,7 @@ jobs: run: | git config --system core.autocrlf false - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Build shell: pwsh @@ -480,7 +478,7 @@ jobs: .\projects\scripts\patch_commit_number.ps1 .\projects\windows\ecode\build.app.ps1 - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false @@ -501,7 +499,7 @@ jobs: run: | git config --system core.autocrlf false - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: { fetch-depth: 0, submodules: 'recursive' } - name: Build shell: pwsh @@ -509,7 +507,7 @@ jobs: .\projects\scripts\patch_commit_number.ps1 .\projects\windows\ecode\build.app.ps1 -arch arm64 - name: Upload Files - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.release.outputs.version }} draft: false diff --git a/.github/workflows/eepp-android-build-check.yml b/.github/workflows/eepp-android-build-check.yml index 7432adee5..ce8a4205f 100644 --- a/.github/workflows/eepp-android-build-check.yml +++ b/.github/workflows/eepp-android-build-check.yml @@ -6,7 +6,7 @@ jobs: Android: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - uses: actions/setup-java@v4 diff --git a/.github/workflows/eepp-ios-build-check.yml b/.github/workflows/eepp-ios-build-check.yml index bef241821..3c932b5ec 100644 --- a/.github/workflows/eepp-ios-build-check.yml +++ b/.github/workflows/eepp-ios-build-check.yml @@ -6,7 +6,7 @@ jobs: iOS: runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Install dependencies diff --git a/.github/workflows/eepp-linux-build-check.yml b/.github/workflows/eepp-linux-build-check.yml index e2bd3cf3c..d9e628a65 100644 --- a/.github/workflows/eepp-linux-build-check.yml +++ b/.github/workflows/eepp-linux-build-check.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 2 @@ -19,7 +19,7 @@ jobs: sudo add-apt-repository -y universe sudo add-apt-repository -y multiverse sudo apt update - sudo apt install -y gcc-13 g++-13 wget libsdl2-2.0-0 libsdl2-dev mesa-utils xvfb + sudo apt install -y gcc-13 g++-13 wget libsdl2-2.0-0 libsdl2-dev mesa-utils xvfb gdb wget https://cdn.ensoft.dev/eepp-assets/premake-5.0.0-beta6-linux.tar.gz tar xvzf premake-5.0.0-beta6-linux.tar.gz - name: Build @@ -29,8 +29,8 @@ jobs: make all -j$(nproc) -e config=release_x86_64 - name: Unit Tests run: | - cd bin/unit_tests - xvfb-run ./eepp-unit_tests + cd projects/scripts + bash ./run_gdb_tests.sh - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/eepp-macos-build-check.yml b/.github/workflows/eepp-macos-build-check.yml index f3ba3b148..462c0c98c 100644 --- a/.github/workflows/eepp-macos-build-check.yml +++ b/.github/workflows/eepp-macos-build-check.yml @@ -7,7 +7,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 2 diff --git a/.github/workflows/eepp-windows-build-check.yml b/.github/workflows/eepp-windows-build-check.yml index 070ec1eed..a2bd56246 100644 --- a/.github/workflows/eepp-windows-build-check.yml +++ b/.github/workflows/eepp-windows-build-check.yml @@ -13,7 +13,7 @@ jobs: run: | git config --system core.autocrlf false - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: ref: ${{ github.ref }} fetch-depth: 2 diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 0983dc14e..875250b0c 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -202,7 +202,7 @@ Dateipfad: Angepasste Variablen Angepasste Variablen erlauben das Vereinfachen von Build-Befehlsschritten durch Hinzufügen eigener Variablen, die über die Build-Einstellungen in Befehlen, Argumenten und Arbeitsverzeichnissen angewendet werden können. Angepasste Variablen können mit ${variable_name} in beliebigen Befehlen aufgerufen werden. - Es gibt bereits vordefinierte angepasste Variablen zur Nutzung:
${project_root}: Ordner-/Projekt-Root-Verzeichnis.
${build_type}: Ausgewählter Build-Typ zur Projekterstellung.
${os}: Name des aktuellen Betriebssystems.
${arch}: Architektur des aktuellen Betriebssystems.
${nproc}: Anzahl logischer Prozessoreinheiten.
${current_doc}: Letzter oder aktueller Dokumentenpfad.
${current_doc_name}: Letzter oder aktueller Name fokussierten Dokuments (ohne Erweiterung).
${current_doc_dir}: Letztes oder gegenwärtig fokussiertes Projektverzeichnis. + Es gibt bereits vordefinierte angepasste Variablen zur Nutzung: ${project_root}: Das Projekt-Stammverzeichnis. ${build_type}: Der aktuell ausgewählte Build-Typ. ${os}: Der Name des aktuellen Betriebssystems. ${arch}: Die Architektur des aktuellen Betriebssystems. ${nproc}: Die Anzahl der logischen Prozessorkerne. ${current_doc}: Der Pfad des aktuell fokussierten Dokuments. ${current_doc_name}: Der Name des aktuell fokussierten Dokuments (ohne Erweiterung). ${current_doc_dir}: Das Verzeichnis des aktuell fokussierten Dokuments. ${relative_dir}: Der relative Verzeichnispfad zwischen ${project_root} und ${current_doc_dir}. Ausschneiden Dunkel Datum @@ -280,6 +280,9 @@ Soll es jetzt heruntergeladen werden? Farbwähler aktivieren Aktiviert den Farbwähler bei Doppelklick auf farbenrepräsentierende Wörter. + Farbkästen aktivieren + Rendert ein Hintergrundfeld mit der analysierten Farbe direkt hinter dem +Text selbst. Farbvorschau aktivieren Aktiviert die Farbvorschau beim Halten der Maus über farbenrepräsentierenden Wörtern. diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index ca24ef83d..aa95864aa 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -186,7 +186,7 @@ File path is: Custom Variables Custom Variables allow to simplify the build commands steps adding custom variables that can be used over the build settings in commands, arguments, and working directories. Custom Variables can be invoked using ${variable_name} in any of the commands. - There are predefined custom variables available to use: ${project_root}: The folder / project root directory. ${build_type}: The build type selected to build the project. ${os}: The current operating system name. ${arch}: The current operating architecture. ${nproc}: The number of logical processing units. ${current_doc}: The last or current focused document path. ${current_doc_name}: The last or current focused document name without extension. ${current_doc_dir}: The last or current focused document directory. + There are predefined custom variables available to use: ${project_root}: The project root directory. ${build_type}: The currently selected build type. ${os}: The current operating system name. ${arch}: The current operating system architecture. ${nproc}: The number of logical processor cores. ${current_doc}: The path of the currently focused document. ${current_doc_name}: The name of the currently focused document (without extension). ${current_doc_dir}: The directory of the currently focused document. ${relative_dir}: The relative directory path between ${project_root} and ${current_doc_dir}. Cut Dark Date @@ -264,6 +264,9 @@ Do you want to download it now? Enable Color Picker Enables the color picker tool when a double click selection is done over a word representing a color. + Enable Color Boxes + Renders a background box of the parsed color directly behind the +text itself. Enable Color Preview Enables a quick preview of a color when the mouse is hover a word that represents a color. diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index 80239e4c0..601cd9367 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -187,15 +187,7 @@ Le chemin du fichier est : Variables personnalisées Les variables personnalisées permettent de simplifier les étapes des commandes de construction en ajoutant des variables personnalisées qui peuvent être utilisées sur les paramètres de construction dans les commandes, les arguments et les répertoires de travail. Les variables personnalisées peuvent être appelées à l'aide de ${nom_variable} dans n'importe laquelle des commandes. - Il existe des variables personnalisées prédéfinies disponibles : -${project_root} : le dossier / répertoire racine du projet. -${build_type} : le type de construction sélectionné pour construire le projet. -${os} : le nom du système d'exploitation courant. -${arch} : l'architecture d'exploitation courante. -${nproc} : le nombre de processeurs. -${current_doc} : le chemin du document courant ou le plus récent. -${current_doc_name} : le nom du document courant ou le plus récent sans extension. -${current_doc_dir} : le répertoire du document courant ou le plus récent. + Il existe des variables personnalisées prédéfinies disponibles : ${project_root} : le répertoire racine du projet. ${build_type} : le type de construction actuellement sélectionné. ${os} : le nom du système d'exploitation actuel. ${arch} : l'architecture du système d'exploitation actuel. ${nproc} : le nombre de cœurs de processeurs logiques. ${current_doc} : le chemin du document actuellement focalisé. ${current_doc_name} : le nom du document actuellement focalisé (sans extension). ${current_doc_dir} : le répertoire du document actuellement focalisé. ${relative_dir} : le chemin du répertoire relatif entre ${project_root} et ${current_doc_dir}. Couper Sombre Date @@ -272,6 +264,9 @@ Voulez-vous le télécharger maintenant ? Activer le rechargement automatique Activer le sélecteur de couleurs Active l'outil de sélection de couleurs lorsqu'un double clic est effectué sur un mot représentant une couleur. + Activer les boîtes de couleur + Affiche une zone d'arrière-plan de la couleur analysée directement derrière le +texte lui-même. Activer l'aperçu des couleurs Activer un aperçu rapide d'une couleur lorsque la souris survole un mot qui représente une couleur. Activer la barre de défilement horizontale @@ -855,7 +850,7 @@ dans l'arborescence de répertoires ainsi que dans les boites de dialogues de s Supprimer Ouvrir le dossier parent dans l'explorateur de fichiers Coller - Refaire/string> + Refaire Sélectionner tout Annuler Copier @@ -918,3 +913,5 @@ Redémarrer ecode pour voir les changements. Coller dans le terminal Renommer le terminal +me="terminal_rename">Renommer le terminal + diff --git a/bin/assets/i18n/zh_CN.xml b/bin/assets/i18n/zh_CN.xml index cdb379828..70b22e549 100644 --- a/bin/assets/i18n/zh_CN.xml +++ b/bin/assets/i18n/zh_CN.xml @@ -16,8 +16,8 @@ license: 'MIT (respective)' ?--> - - 关于Ecode + + 关于 Ecode 新增分支 新增构建 新增构建步骤 @@ -61,29 +61,27 @@ 删除构建名称 %s? 构建类型必填! 构建类型 - Build types can be used as a dynamic build option represented by the special key ${build_type}. The build type can be switch easily from the editor. + 构建类型可用作由特殊键 ${build_type} 表示的动态构建选项。可以在编辑器中轻松切换构建类型。 取消 取消构建 取消清理 - Capture Positions - Case sensitive - Change Case - Change Escape Sequence - Change Whole Word - Always check for new updates at startup. - Check For Updates - Check Languages Health - Clean - Clean run with errors - + 捕捉位置 + 区分大小写 + 切换大小写 + 更改转义序列 + 更改全词匹配 + 启动时总是检查更新。 + 检查更新 + 检查语言健康状况 + 清理 + 清理运行时出错 清理步骤 - 成功清理run - + 成功清理运行 清理历史 清空菜单 - Clear System Environment - Clone Setting - Cloned name must be different from any existing build name. + 清除系统环境 + 克隆设置 + 克隆名称必须与任何现有构建名称不同。 关闭 关闭所有标签页 关闭Ecode @@ -151,21 +149,21 @@ 自定义环境变量 自定义输出解析器 自定义环境变量 - Custom Variables allow to simplify the build commands steps adding custom variables that can be used over the build settings in commands, arguments, and working directories. - Custom Variables can be invoked using ${variable_name} in any of the commands. - There are predefined custom variables available to use:
${project_root}: The folder / project root directory.
${build_type}: The build type selected to build the project.
${os}: The current operating system name.
${nproc}: The number of logical processing units. + 自定义变量允许通过添加自定义变量来简化构建命令步骤,这些变量可以在构建设置的命令、参数和工作目录中使用。 + 可以在任何命令中使用 ${variable_name} 来调用自定义变量。 + 有以下预定义的自定义变量可用: ${project_root}: 项目根目录。 ${build_type}: 当前选中的构建类型。 ${os}: 当前操作系统名称。 ${arch}: 当前操作系统架构。 ${nproc}: 逻辑处理器核心数。 ${current_doc}: 当前选中的文档路径。 ${current_doc_name}: 当前选中的文档名称(不含扩展名)。 ${current_doc_dir}: 当前选中的文档所在目录。 ${relative_dir}: ${project_root} 与 ${current_doc_dir} 之间的相对目录路径。 剪切 - Dark + 深色 日期 - Draw Boxes - Debug Draw Boxes Toggle - Debug Draw Debug Data - Highlight Focus & Hover - Debug Draw Highlight Toggle - Debug Widget Tree View - Default Theme - Delete Selected Custom Output Parser - Delete Selected Environment Variable + 绘制框 + 调试绘制框切换 + 调试绘制调试数据 + 高亮焦点和悬停 + 调试绘制高亮切换 + 调试组件树视图 + 默认主题 + 删除选定的自定义输出解析器 + 删除选定的环境变量 删除 删除已选 删除设置 @@ -197,6 +195,8 @@ 开启颜色选择器 开启颜色选择工具 当光标悬浮在颜色字符串上时 + 启用色框 + 直接在文本后面呈现已解析颜色的背景框。 开启颜色预览 允许颜色预览 当鼠标悬停在颜色字符串上时 @@ -445,8 +445,8 @@ Restart ecode to see the changes. 打开工作区符号搜索 输出 输出解析器 - Custom output parsers scan command line output for user-provided error patterns to create entries in Build Issues and highlight those errors on the Build Output - Presets are provided as generic output parsers, you can select one below, by default a "generic" preset will be selected: + 自定义输出解析器扫描命令行输出,查找用户提供的错误模式,以在构建问题中创建条目并在构建输出上突出显示这些错误。 + 预设提供为通用输出解析器,您可以在下面选择一个,默认情况下将选择“通用”预设: 覆盖 主人 粘贴 @@ -574,7 +574,7 @@ Restart ecode to see the changes. 状态 支持的平台 - Selecting none means that the build settings will work and be available on any Operating System + 不选择任何内容表示构建设置将在任何操作系统上工作并可用。 选择构建 选择构建类型 选择侧栏 @@ -630,21 +630,21 @@ file in the directory tree. Type Type to Locate Ui字体大小 - UI语言 Language - Multisample Anti-Aliasing Level - Ui板字体大小 - UI配色方案 + 界面语言 + 多重采样抗锯齿级别 + 界面面板字体大小 + 界面偏好配色方案 渲染器 渲染器版本 - Ui缩放 - UI主题 - Copy - Copy Containing Folder Path... - Copy File Path - Copy File Path and Position - Paste - Delete - Open Containing Folder in File Manager + 界面缩放比例 + 界面主题 + 复制 + 复制所在文件夹路径... + 复制文件路径 + 复制文件路径和位置 + 剪切 + 删除 + 在文件管理器中打开所在文件夹 粘贴 重做 全选 diff --git a/bin/assets/layouts/test.xml b/bin/assets/layouts/test.xml index 80559f2a6..156a532d1 100644 --- a/bin/assets/layouts/test.xml +++ b/bin/assets/layouts/test.xml @@ -49,8 +49,8 @@ - - + + diff --git a/bin/assets/layouts/test_widgets.xml b/bin/assets/layouts/test_widgets.xml index e00ceb08d..eb1dfa9f1 100644 --- a/bin/assets/layouts/test_widgets.xml +++ b/bin/assets/layouts/test_widgets.xml @@ -31,7 +31,7 @@ Test 4Test 5Test 6Test 7Test 8 - + diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json index 71d4b977c..b190a2ef3 100644 --- a/bin/assets/plugins/aiassistant.json +++ b/bin/assets/plugins/aiassistant.json @@ -450,9 +450,8 @@ "display_name": "kimi-k2.5" }, { - "name": "stepfun/step-3.5-flash:free", - "display_name": "StepFun: Step 3.5 Flash (free)", - "cheapest": true + "name": "stepfun/step-3.5-flash", + "display_name": "StepFun: Step 3.5 Flash" }, { "name": "z-ai/glm-4.7", @@ -468,11 +467,16 @@ }, { "name": "arcee-ai/trinity-large-preview:free", - "display_name": "Arcee AI: Trinity Large Preview (free)" + "display_name": "Arcee AI: Trinity Large Preview (free)", + "cheapest": true }, { "name": "nvidia/nemotron-3-super-120b-a12b:free", "display_name": "NVIDIA: Nemotron 3 Super (free)" + }, + { + "name": "minimax/minimax-m2.5:free", + "display_name": "MiniMax: MiniMax M2.5 (free)" } ] }, @@ -492,6 +496,22 @@ { "name": "moonshotai/kimi-k2.5", "display_name": "kimi-k2.5" + }, + { + "name": "z-ai/glm4.7", + "display_name": "GLM-4.7" + }, + { + "name": "minimaxai/minimax-m2.7", + "display_name": "MiniMax M2.7" + }, + { + "name": "stepfun-ai/step-3.5-flash", + "display_name": "Step 3.5 Flash" + }, + { + "name": "deepseek-ai/deepseek-v3.2", + "display_name": "DeepSeek-V3.2" } ] }, @@ -499,6 +519,33 @@ "api_url": "http://localhost:8080/api/chat", "fetch_models_url": "http://localhost:8080/api/tags", "open_api": true + }, + "together": { + "api_url": "https://api.together.xyz/v1/chat/completions", + "display_name": "Together AI", + "models": [ + { + "name": "zai-org/GLM-5.1", + "display_name": "GLM 5.1 FP4" + }, + { + "name": "moonshotai/Kimi-K2.5", + "display_name": "Kimi K2.5" + }, + { + "name": "MiniMaxAI/MiniMax-M2.7", + "display_name": "MiniMax M2.7 FP4" + }, + { + "name": "MiniMaxAI/MiniMax-M2.5", + "display_name": "MiniMax M2.5 FP4" + }, + { + "name": "Qwen/Qwen3.5-9B", + "display_name": "Qwen3.5 9B FP8", + "cheapest": true + } + ] } }, "agents": { @@ -525,6 +572,16 @@ "enabled": true, "command": "pi-acp", "args": [] + }, + "codex-acp": { + "enabled": true, + "command": "codex-acp", + "args": [] + }, + "qwen-code": { + "enabled": true, + "command": "qwen", + "args": ["--acp"] } } } diff --git a/bin/assets/plugins/formatters.json b/bin/assets/plugins/formatters.json index ecced69f4..98470ce85 100644 --- a/bin/assets/plugins/formatters.json +++ b/bin/assets/plugins/formatters.json @@ -2,9 +2,6 @@ "config": { "auto_format_on_save": false }, - "keybindings": { - "format-doc": "alt+f" - }, "formatters": [ { "language": ["javascript", "typescript", "jsx", "tsx", "html"], diff --git a/bin/assets/plugins/linters.json b/bin/assets/plugins/linters.json index b1c2a2bb4..95f124214 100644 --- a/bin/assets/plugins/linters.json +++ b/bin/assets/plugins/linters.json @@ -133,6 +133,14 @@ "command": "yaml", "type": "native", "url": "#native" + }, + { + "language": "html", + "file_patterns": ["%.[mpx]?html?$", "%.handlebars$"], + "warning_pattern": "", + "command": "html", + "type": "native", + "url": "#native" } ] } diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index c24662e92..f10fd3e86 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -52,7 +52,7 @@ droppable-hovering-color: #FFFFFF20; } -markdownview > *, +MarkdownView > *, body { color: var(--font); } @@ -78,42 +78,57 @@ em { } h1 { - font-size: 32dp; + font-size: 2em; margin: 0.67em 0; + font-weight: bold; } h2 { - font-size: 24dp; + font-size: 1.5em; margin: 0.83em 0; + font-weight: bold; } h3 { - font-size: 18dp; - margin: 1.00em 0; + font-size: 1.17em; + margin: 1em 0; + font-weight: bold; } h4 { - font-size: 16dp; + font-size: 1em; margin: 1.33em 0; + font-weight: bold; } h5 { - font-size: 13dp; + font-size: 0.83em; margin: 1.67em 0; + font-weight: bold; } h6 { - font-size: 11dp; - margin: 1.67em 0; + font-size: 0.67em; + margin: 2.33em 0; + font-weight: bold; } -code { - font-family: monospace; - background-color: var(--list-back); +table, td { + text-align: left; } -p, ol, ul, pre { - margin: 1em 0; +hr { + min-height: 1dp; + background-color: gray; + margin: 0.5em 0; +} + +center { + text-align: center; +} + +p, ol, ul, pre, blockquote { + margin: 1em 0; } li > p { @@ -121,35 +136,70 @@ li > p { } ol, ul { - margin-left: 2em; + margin: 0.67em 0; } -li { - margin: 0.67em 0; - padding-left: 2em; +ul > li, +ol > li { + padding-left: 2em; +} + +ol > li { + background-tint: var(--font); + background-position: 0.6em 0.3em; +} + +ol > li:nth-child(1) { + background-image: glyph("monospace", 1em, "1"); +} + +ol > li:nth-child(2) { + background-image: glyph("monospace", 1em, "2"); +} + +ol > li:nth-child(3) { + background-image: glyph("monospace", 1em, "3"); +} + +ol > li:nth-child(4) { + background-image: glyph("monospace", 1em, "4"); +} + +ol > li:nth-child(5) { + background-image: glyph("monospace", 1em, "5"); +} + +ol > li:nth-child(6) { + background-image: glyph("monospace", 1em, "6"); +} + +ol > li:nth-child(7) { + background-image: glyph("monospace", 1em, "7"); +} + +ol > li:nth-child(8) { + background-image: glyph("monospace", 1em, "8"); +} + +ol > li:nth-child(9) { + background-image: glyph("monospace", 1em, "9"); +} + +ul > li { background-image: url("data:image/svg,"); background-tint: var(--font); - background-position: 0.6em 0.5em; - background-size: 0.8em 0.8em; + background-position: 0.6em 0.45em; + background-size: 0.5em 0.5em; } a { - color: var(--primary); - selection-color: var(--font-selected-pressed); - selection-back-color: var(--primary); cursor: arrow; text-decoration: none; - gravity: bottom; } a:hover { - color: var(--font-highlight); - cursor: hand; text-decoration: underline; -} - -br { - layout-height: 0; + cursor: hand; } img { @@ -158,30 +208,70 @@ img { layout-height: wrap_content; } -markdownview { - background-color: var(--list-back); - padding: 4dp; -} - -markdownview h1, -markdownview h2 { - border-bottom: 1dp solid var(--tab-line); -} - -markdownview img { - max-width: 100%; - max-height: 100vh; -} - -markdownview table > thead > tr > th { - font-style: bold; +body img { + scale-type: expand; } blockquote { padding-left: 8dp; background-color: var(--list-back); border-left: 2dp solid var(--tab-line); - margin: 0.67em 0; +} + +blockquote > *:first-child { + margin-top: 0dp; +} + +blockquote > *:last-child { + margin-bottom: 0dp; +} + +MarkdownView p, +MarkdownView ol, +MarkdownView ul, +MarkdownView pre, +MarkdownView blockquote { + margin-top: 0; +} + +MarkdownView a { + color: var(--primary); + selection-color: var(--font-selected-pressed); + selection-back-color: var(--primary); + gravity: bottom; +} + +MarkdownView a:hover { + color: var(--font-highlight); +} + +MarkdownView { + background-color: var(--list-back); + padding: 4dp; +} + +MarkdownView h1, +MarkdownView h2 { + border-bottom: 1dp solid var(--tab-line); +} + +MarkdownView img { + scale-type: fit-inside; + max-width: 100%; + max-height: 100vh; +} + +MarkdownView table > thead > tr > th { + font-style: bold; +} + +MarkdownView CodeEditor { + padding: 4dp; +} + +MarkdownView code { + font-family: monospace; + background-color: var(--button-back); } pushbutton, @@ -225,7 +315,7 @@ SpinBox::input, Tab, TextEdit, TextInput, -TextInputPassword, +TextArea, TextView, Anchor, Tooltip, @@ -242,8 +332,8 @@ listview::cell { TextView, Anchor, TextEdit, +TextArea, TextInput, -TextInputPassword, ComboBox::DropDownList, SpinBox::input, { selection-color: var(--font-selected-pressed); @@ -307,7 +397,8 @@ SelectButton:selected:pressed { pushbutton:disabled, selectbutton:disabled, textinput:disabled, -textedit:disabled { +textedit:disabled, +textarea:disabled { color: var(--disabled-color); border-color: var(--disabled-border); } @@ -392,7 +483,7 @@ RadioButton::active { ListBox, DropDownList::ListBox, ComboBox::DropDownList::ListBox, -Table, +MarkdownView Table, ListView { background-color: var(--list-back); border-color: var(--button-border); @@ -455,8 +546,7 @@ ComboBox::DropDownList::ListBox::item:selected { tint: var(--font-selected-pressed); } -TextInput, -TextInputPassword { +TextInput { padding-left: var(--base-horizontal-padding); padding-right: var(--base-horizontal-padding); padding-top: var(--base-vertical-padding); @@ -484,8 +574,6 @@ ComboBox::DropDownList { padding-bottom: var(--base-vertical-padding); } -TextInputPassword:hover, -TextInputPassword:focus, TextInput:hover, TextInput:focus, SpinBox:hover, @@ -521,7 +609,8 @@ SpinBox::btndown { height: 13dp; } -TextEdit { +TextEdit, +TextArea { background-color: var(--list-back); border-color: var(--button-border); border-radius: var(--button-radius); @@ -534,7 +623,9 @@ TextEdit { } TextEdit:focus, -TextEdit:hover { +TextEdit:hover, +TextArea:focus, +TextArea:hover { border-color: var(--primary); } @@ -1035,6 +1126,7 @@ Menu::RadioButton::icon:selected { ListBox > ScrollBar, TextEdit > ScrollBar, +TextArea > ScrollBar, Table > ScrollBar { background-color: var(--list-back); } diff --git a/bin/unit_tests/assets/html/base.css b/bin/unit_tests/assets/html/base.css new file mode 100644 index 000000000..ce116e654 --- /dev/null +++ b/bin/unit_tests/assets/html/base.css @@ -0,0 +1,389 @@ +body { + font-size: 11px; + color: black; + background-color: white; +} + +h1 { + font-size: 2em; + margin-top: 0.67em; + margin-right: 0; + font-weight: bold; +} + +h2 { + font-size: 1.5em; + margin-top: 0.83em; + margin-right: 0; + font-weight: bold; +} + +h3 { + font-size: 1.17em; + margin-top: 1em; + margin-right: 0; + font-weight: bold; +} + +h4 { + font-size: 1em; + margin-top: 1.33em; + margin-right: 0; + font-weight: bold; +} + +h5 { + font-size: 0.83em; + margin-top: 1.67em; + margin-right: 0; + font-weight: bold; +} + +h6 { + font-size: 0.67em; + margin-top: 1.67em; + margin-right: 0; + font-weight: bold; +} + +p { + margin-top: 1em; + margin-right: 0; +} + +blockquote { + margin-top: 1em; + margin-right: 0; +} + +dd { + margin-top: 1em; + margin-right: 0; +} + +dl { + margin-top: 1em; + margin-right: 0; +} + +ol { + margin-top: 1em; + margin-right: 0; +} + +ul { + margin-top: 1em; + margin-right: 0; +} + +figure { + margin-top: 1em; + margin-right: 0; +} + +pre { + margin-top: 1em; + margin-right: 0; +} + +blockquote { + margin-top: 1em; + margin-right: 40px; +} + +pre { + font-family: monospace; + font-size: 1em; +} + +code { + font-family: monospace; + font-size: 1em; +} + +kbd { + font-family: monospace; + font-size: 1em; +} + +samp { + font-family: monospace; + font-size: 1em; +} + +tt { + font-family: monospace; + font-size: 1em; +} + +var { + font-family: monospace; + font-size: 1em; +} + +pre { + margin-top: 1em; + margin-right: 0; +} + +ul { + margin-top: 1em; + margin-right: 0; +} + +ol { + list-style-type: decimal; + margin-top: 1em; + margin-right: 0; +} + +li { + text-align: match-parent; +} + +b { + font-weight: bold; +} + +strong { + font-weight: bold; +} + +i { + font-style: italic; +} + +em { + font-style: italic; +} + +cite { + font-style: italic; +} + +u { + text-decoration: underline; +} + +ins { + text-decoration: underline; +} + +s { + text-decoration: line-through; +} + +strike { + text-decoration: line-through; +} + +del { + text-decoration: line-through; +} + +big { + font-size: 16dp; +} + +small { + font-size: 9dp; +} + +sub { + vertical-align: sub; + font-size: 9dp; +} + +sup { + vertical-align: super; + font-size: 9dp; +} + +a:link { + color: #0000EE; + text-decoration: underline; +} + +a:visited { + color: #551A8B; + text-decoration: underline; +} + +th { + font-weight: bold; + text-align: center; +} + +hr { + margin-top: 0.5em; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + color: gray; +} + +b, +strong { + font-style: bold; +} + +u, +ins { + text-decoration: underline; +} + +s, +del { + text-decoration: strikethrough; +} + +i, +em { + font-style: italic; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; + font-weight: bold; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; + font-weight: bold; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; + font-weight: bold; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; + font-weight: bold; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; + font-weight: bold; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; + font-weight: bold; +} + +table, td { + text-align: left; +} + +hr { + min-height: 1dp; + background-color: gray; + margin: 0.5em 0; +} + +center { + text-align: center; +} + +p, ol, ul, pre, blockquote { + margin: 1em 0; +} + +li > p { + margin: 0; +} + +ol, ul { + margin: 0.67em 0; +} + +ul > li, +ol > li { + padding-left: 2em; +} + +ol > li { + background-position: 0.6em 0.3em; +} + +ol > li:nth-child(1) { + background-image: glyph("monospace", 1em, "1"); +} + +ol > li:nth-child(2) { + background-image: glyph("monospace", 1em, "2"); +} + +ol > li:nth-child(3) { + background-image: glyph("monospace", 1em, "3"); +} + +ol > li:nth-child(4) { + background-image: glyph("monospace", 1em, "4"); +} + +ol > li:nth-child(5) { + background-image: glyph("monospace", 1em, "5"); +} + +ol > li:nth-child(6) { + background-image: glyph("monospace", 1em, "6"); +} + +ol > li:nth-child(7) { + background-image: glyph("monospace", 1em, "7"); +} + +ol > li:nth-child(8) { + background-image: glyph("monospace", 1em, "8"); +} + +ol > li:nth-child(9) { + background-image: glyph("monospace", 1em, "9"); +} + +ul > li { + background-image: url("data:image/svg,"); + background-position: 0.6em 0.45em; + background-size: 0.5em 0.5em; +} + +a { + cursor: arrow; + text-decoration: none; +} + +a:hover { + text-decoration: underline; + cursor: hand; +} + +img { + scale-type: fit-inside; + layout-width: wrap_content; + layout-height: wrap_content; +} + +body img { + scale-type: expand; +} + +blockquote { + padding-left: 8dp; +} + +blockquote > *:first-child { + margin-top: 0dp; +} + +blockquote > *:last-child { + margin-bottom: 0dp; +} diff --git a/bin/unit_tests/assets/html/blog_main_incorrect_widths.html b/bin/unit_tests/assets/html/blog_main_incorrect_widths.html new file mode 100644 index 000000000..3372c0f55 --- /dev/null +++ b/bin/unit_tests/assets/html/blog_main_incorrect_widths.html @@ -0,0 +1,376 @@ + + + + + + + + + + diff --git a/bin/unit_tests/assets/html/dwarmstrong/dwarmstrong.html b/bin/unit_tests/assets/html/dwarmstrong/dwarmstrong.html new file mode 100644 index 000000000..ee08d3c65 --- /dev/null +++ b/bin/unit_tests/assets/html/dwarmstrong/dwarmstrong.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + Daniel Wayne Armstrong + + + + + + +
+ +
+

meHello! I'm Daniel. Welcome to my blog. Here are all my posts. +

+

I love free/libre software, run Linux or BSD on every computer I get my hands on, and think a good book paired with a cup of coffee is a little piece of Heaven. +

+
+

Latest Posts

+ +
+
+ +
+ +
+
+

The impediment to action advances action. What stands in the way becomes the Way. — Marcus Aurelius
+ © 2026 Daniel Wayne Armstrong + Contact + Created with using Zola +

+
+ + diff --git a/bin/unit_tests/assets/html/dwarmstrong/style.css b/bin/unit_tests/assets/html/dwarmstrong/style.css new file mode 100644 index 000000000..868f62ce2 --- /dev/null +++ b/bin/unit_tests/assets/html/dwarmstrong/style.css @@ -0,0 +1,212 @@ +/* + * Reaction Time Is A Factor In This + */ + +body { + background-color: #000; + color: #d8dee9; + max-width: 960px; + margin: 1.5rem auto !important; + padding: 0 1.5rem; + float: none !important; + font-family: system-ui, sans-serif; + font-size: 1.2rem; + line-height: 1.45em; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "yanone_kaffeesatz", sans-serif; + color: #8fbcbb; +} + +h1 { + font-size: 2.75rem; + line-height: 2.25rem; + margin: 1.9rem 0 0 0; +} + +h2 {font-size: 2.25rem; line-height: 2.15rem;} + +h3, h4, h5, h6 {font-size: 1.75rem;} + +p.breadcrumbs {color: #8fbcbb;} + +p.mastodon { + text-align: center; +} + +p.page-next { + text-align: right; + margin-top: 0.65rem; + margin-bottom: -0.5rem; +} + +p.tags {font-size: 1.25rem; line-height: 2rem;} + +a { + color: #81a1c1; + text-decoration: none; + border-bottom: 2px dashed #4c566a; +} + +a:hover {color: #bf616a; border-bottom: none;} + +header a { + font-family: "yanone_kaffeesatz", sans-serif; + font-size: 1.5rem; + text-decoration: none; + border-bottom: none; +} + +blockquote { + border-left: 0.4rem solid #8fbcbb; + font-style: italic; + padding: 0 1.0em; +} + +code { + background-color: #2e3440; + padding: 0.1rem 0.2rem; + font-family: monospace; + font-size: 1rem; +} + +pre code { + border-left: 0.4rem solid #8fbcbb; + page-break-inside: avoid; + padding: .5rem 1rem; + line-height: 1.2rem; + font-size: 1.1rem; + max-width: 100%; + display: block; + overflow: auto; + overflow-x: auto; +} + +img {max-width: 100%;} + +img.centre {display: block; margin: 1rem auto;} + +img.floatleft {float: left; margin: 0 1rem 1rem 0;} + +img.floatright {float: right; margin: 0 0 1rem 1rem;} + +/* margin: top/right/bottom/left */ +/* img.me {float: right; margin: 0 0 1rem 1rem; border-radius: 5%;} */ +img.me {float: left; margin: 0 1rem 1rem 0; border-radius: 5%;} + +.clear-float { + clear: both; /* This element will appear below the floated image */ +} + +ul {list-style: square; padding-left: 1.2rem;} + +ul li {padding-bottom: 0.5rem;} + +ul.latest-list { + font-size: 1.2rem; + line-height: 1.45em; +} + +ul.page-list a, ul.latest-list a { + font-size: 1.85rem; + font-family: "yanone_kaffeesatz", sans-serif; + line-height: 2rem; +} + +hr {color: #8fbcbb;} + +footer { + text-align: center; + font-size: 1rem; + margin-top: 1.5em; +} + +#greeting { + font-size: 1.35rem; + line-height: 1.3em; +} + +#fossBanner { + background-image: url(img/foss-banner.png); + display: block; + /* text-indent: -9999px; */ + width: 880px; + height: 120px; + margin: 0 auto; + border-bottom: none; +} + +#fossSubtitle p { + font-family: monospace; + font-size: 1.1rem; + text-align: center; + margin-top: 0; + color: #ff3131; +} + +#fossQuote p { + text-align: right; + margin-top: 1.5rem; +} + +#mainMenu { + font-size: 1.45rem; + margin-bottom: 1.2rem; +} + +.author {font-weight: bold; color: #ebcb8b;} + +.rss {color: #f26522;} + +.boldWords {font-weight: bold;} + +.clear {clear: both;} + +.bottomMenu {margin-top: 2em;} + +.feed {color: #d08770;} + +.love {color: #ff3131;} + +.meta {margin-top: 0.5rem; color: #5d6d7e;} + +.separator {color: #8fbcbb; padding: 0 0.4rem;} + +.tag-count{vertical-align: super; font-size: 1rem;} + +/* Separators */ +/* See https://stackoverflow.com/a/26634224 */ + +.readMore { + display: flex; + align-items: center; + text-align: center; + color: #8fbcbb; + margin-top: 1.5rem; +} + +.readMore::before, +.readMore::after { + content: ''; + flex: 1; + border-bottom: 1px solid #8fbcbb; +} + +.readMore:not(:empty)::before {margin-right: .25em;} + +.readMore:not(:empty)::after {margin-left: .25em;} + +/* Footnotes */ + +.footnote-definition p{display:inline} + +.footnote-definition+.footnote-definition{margin-top:1em} + +.footnote-reference,.footnote-definition-label{text-decoration:none} + +.footnote-reference:before,.footnote-definition-label:before{content:"["} + +.footnote-reference:after,.footnote-definition-label:after{content:"]"} + +.footnote-reference a{text-decoration:none} diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp new file mode 100644 index 000000000..f658020cb Binary files /dev/null and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp differ diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp new file mode 100644 index 000000000..340f7f8ee Binary files /dev/null and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp differ diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp new file mode 100644 index 000000000..82dd9da68 Binary files /dev/null and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp differ diff --git a/bin/unit_tests/assets/html/hn_frontpage.html b/bin/unit_tests/assets/html/hn_frontpage.html new file mode 100644 index 000000000..e52fde3da --- /dev/null +++ b/bin/unit_tests/assets/html/hn_frontpage.html @@ -0,0 +1,1837 @@ + + + + + + + + + Hacker News + + +
+ + + + + + + + + + + + + +
+ + + + + + + + +
+ + + Hacker Newsnew | + threads | + past | + comments | + ask | show | + jobs | + submit + + SpartanJ (209) | + logout +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1. + + The Claude Code Source Leak: fake tools, frustration + regexes, undercover mode + (alex000kim.com) +
+ 365 points + by + alex000kim + 4 hours ago + | + flag + | + hide + | 150 comments +
+ 2. + + OpenAI raises $122B + (cnbc.com) +
+ 58 points + by + surprisetalk + 1 hour ago + | + flag + | + hide + | 50 comments +
+ 3. + + GitHub's Historic Uptime + (damrnelson.github.io) +
+ 273 points + by + todsacerdoti + 2 hours ago + | + flag + | + hide + | 79 comments +
+ 4. + + Cohere Transcribe: Speech Recognition + (cohere.com) +
+ 124 points + by gmays + 4 hours ago + | + flag + | + hide + | 42 comments +
+ 5. + + Slop is not necessarily the future + (greptile.com) +
+ 110 points + by + dakshgupta + 5 hours ago + | + flag + | + hide + | 205 comments +
+ 6. + + Open source CAD in the browser (Solvespace) + (solvespace.com) +
+ 245 points + by + phkahler + 8 hours ago + | + flag + | + hide + | 75 comments +
+ 7. + + I Traced My Traffic Through a Home Tailscale Exit + Node + (stonecharioteer.com) +
+ 18 points + by + stonecharioteer + 1 hour ago + | + flag + | + hide + | 3 comments +
+ 8. + + OkCupid gave 3M dating-app photos to facial + recognition firm, FTC says + (arstechnica.com) +
+ 182 points + by + whiteboardr + 3 hours ago + | + flag + | + hide + | 42 comments +
+ 9. + + Show HN: Postgres extension for BM25 relevance-ranked + full-text search + (github.com/timescale) +
+ 49 points + by tjgreen + 2 hours ago + | + flag + | + hide + | 16 comments +
+ 10. + + Teenage Engineering's PO-32 acoustic modem and synth + implementation + (github.com/ericlewis) +
+ 34 points + by + ericlewis + 2 hours ago + | + flag + | + hide + | 5 comments +
+ 11. + + Nematophagous Fungus + (wikipedia.org) +
+ 21 points + by + lordgilman + 2 hours ago + | + flag + | + hide + | 3 comments +
+ 12. + + Show HN: Forkrun – NUMA-aware shell parallelizer + (50×–400× faster than parallel) + (github.com/jkool702) +
+ 74 points + by + jkool702 + 5 hours ago + | + flag + | + hide + | 10 comments +
+ 13. + + Claude Code's source code has been leaked via a map + file in their NPM registry + (twitter.com/fried_rice) +
+ 1756 points + by treexs + 12 hours ago + | + flag + | + hide + | 867 comments +
+ 14. + + A Primer on Long-Duration Life Support + (mceglowski.substack.com) +
+ 39 points + by zdw + 3 hours ago + | + flag + | + hide + | 12 comments +
+ 15. + + Ministack (Replacement for LocalStack) + (ministack.org) +
+ 4 points + by + kerblang + 19 minutes ago + | + flag + | + hide + | discuss +
+ 16. + + 4D Doom + (github.com/danieldugas) +
+ 5 points + by + chronolitus + 1 hour ago + | + flag + | + hide + | discuss +
+ 17. + + From 300KB to 69KB per Token: How LLM Architectures + Solve the KV Cache Problem + (future-shock.ai) +
+ 45 points + by + future-shock-ai + 5 hours ago + | + flag + | + hide + | 5 comments +
+ 18. + + Accidentally created my first fork bomb with Claude + Code + (droppedasbaby.com) +
+ 37 points + by + offbyone42 + 4 hours ago + | + flag + | + hide + | 7 comments +
+ 19. + + Axios compromised on NPM – Malicious versions drop + remote access trojan + (stepsecurity.io) +
+ 1717 points + by mtud + 18 hours ago + | + flag + | + hide + | 694 comments +
+ 20. + + Audio tapes reveal mass rule-breaking in Milgram's + obedience experiments + (psypost.org) +
+ 177 points + by + lentoutcry + 11 hours ago + | + flag + | + hide + | 111 comments +
+ 21. + + GitHub Monaspace Case Study + (lettermatic.com) +
+ 89 points + by + homebrewer + 6 hours ago + | + flag + | + hide + | 26 comments +
+ 22. + + Securing Elliptic Curve Cryptocurrencies Against + Quantum Vulnerabilities [pdf] + (quantumai.google) +
+ 37 points + by + jandrewrogers + 5 hours ago + | + flag + | + hide + | 17 comments +
+ 23. + + JSSE: A JavaScript Engine Built by an Agent + (ocmatos.com) +
+ 11 points + by tilt + 46 minutes ago + | + flag + | + hide + | 3 comments +
+ 24. + + Combinators + (rubenverg.com) +
+ 117 points + by tosh + 9 hours ago + | + flag + | + hide + | 34 comments +
+ 25. + + Ask HN: Distributed data centers in our basements +
+ 31 points + by cmos + 4 hours ago + | + flag + | + hide + | 50 comments +
+ 26. + + Microsoft: Copilot is for entertainment purposes + only + (microsoft.com) +
+ 357 points + by lpcvoid + 6 hours ago + | + flag + | + hide + | 143 comments +
+ 27. + + Scotty: A beautiful SSH task runner + (freek.dev) +
+ 30 points + by speckx + 4 hours ago + | + flag + | + hide + | 19 comments +
+ 28. + + What major works of literature were written after age + of 85? 75? 65? + (columbia.edu) +
+ 112 points + by + paulpauper + 11 hours ago + | + flag + | + hide + | 77 comments +
+ 29. + + Show HN: PhAIL – Real-robot benchmark for AI + models + (phail.ai) +
+ 17 points + by vertix + 3 hours ago + | + flag + | + hide + | 8 comments +
+ 30. + + Oracle slashes 30k jobs + (rollingout.com) +
+ 784 points + by pje + 6 hours ago + | + flag + | + hide + | 682 comments +
+ +
+
+ + + + + + + +
+
+
+ Guidelines | + FAQ | Lists | + API | + Security | + Legal | + Apply to YC | + Contact

+
+ Search: + +
+
+
+
+ + + diff --git a/bin/unit_tests/assets/html/hn_thread_test.html b/bin/unit_tests/assets/html/hn_thread_test.html new file mode 100644 index 000000000..8369a0e4a --- /dev/null +++ b/bin/unit_tests/assets/html/hn_thread_test.html @@ -0,0 +1,176 @@ + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + +
+ + + Hacker Newsnew | + threads | + past | + comments | ask | + show | jobs | + submit + + SpartanJ (209) | + logout +
+
+ + + + + + + + + + + + + + + + + + + +
+ + + $500 GPU outperforms Claude Sonnet + on coding + benchmarks + (github.com/itigges22) +
+ 107 points + by yogthos + 10 hours + ago + | + flag + | + hide + | + past + | + favorite + | 33 comments +
+
+
+
+
+ + + + +
+ + + + + + +
+ + +
+ mmaunder + 2 hours + ago + + | + + | + + [–] +
+
+
+
+ I’d encourage devs to use MiniMax, Kimi, etc for + real world tasks that require intelligence. The down + sides emerge pretty fast: much higher reasoning + token use, slower outputs, and degradation that is + palpable. Sadly, you do get what you pay for right + now. However that doesn’t prevent you from saving + tons through smart model routing, being smart about + reasoning budgets, and using max output tokens + wisely. And optimize your apps and prompts to reduce + output tokens. +
+
+

+ reply +

+
+
+
+
+

+
+ + + + + +
+
+
+ Guidelines | + FAQ | Lists | + API | + Security | + Legal | + Apply to YC | + Contact

+
+
+
+ + diff --git a/bin/unit_tests/assets/html/hn_threaded_test.html b/bin/unit_tests/assets/html/hn_threaded_test.html new file mode 100644 index 000000000..7925d912e --- /dev/null +++ b/bin/unit_tests/assets/html/hn_threaded_test.html @@ -0,0 +1,1292 @@ + + + + + + + + + The Cognitive Dark Forest | Hacker News + + +
+ + + + + + + + + + + + + +
+ + + + + + + + +
+ + + Hacker Newsnew | + threads | + past | + comments | + ask | show | + jobs | + submit + + SpartanJ (209) | + logout +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + The Cognitive Dark Forest + (ryelang.org) +
+ 312 points + by + kaycebasques + 8 hours ago + | + flag + | + hide + | + past + | + favorite + | 144 comments +
+
+ help

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + +
+ pugio + 7 hours ago + + | + + | + + [–] +
+
+
+
+ Thanks, this helped crystallize something for + me: the play the AI labs are making is + anti-fragile (in the Nassim Taleb sense): +

+ > The very act of resisting feeds what + you resist and makes it less fragile to + future resistance. +

+

+ At least along certain dimensions. I don't + think the labs themselves are antifragile. + Obviously we all know the labs are training + on everything (so write/act the way you want + future AIs to perceive you), but I hadn't + really focused on how they're absorbing the + innovation that they stimulate. There's + probably a biological analog... +

+

+ Well there are many, and I quote this AI + response here for its chilling parallels: +

+

+ > Parasitic castrators and host + manipulators do something related. Some + parasites redirect a host’s resources away + from reproduction and into body maintenance + or altered tissue states that benefit the + parasite. A classic example is parasites + that make hosts effectively become + growth/support machines for the parasite. It + is not always “stimulate more tissue, then + eat it,” but it is + “stimulate more usable host productivity, + then exploit it.” + (ChatGPT 5.4 Thinking. Emphasis mine.) +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ gobdovan + 6 hours ago + + | + + | + + [–] +
+
+
+
+ Instead of anti-fragility, I'd point you to + the law of requisite variety instead. You'll + notice that all AI improvements are insanely + good for a week or two after launch. Then + you'll see people stating that 'models got + worse'. What happened in fact is that people + adapted to the tool, but the tool didn't adapt + anymore. We're using AI as variety resistant + and adaptable tools, but we miss the fact that + most deployments nowadays do not adapt back to + you as fast. +
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ chongli + 6 hours ago + + | + + | + + | + + [–] +
+
+
+
+ New models literally do get worse after + launch, due to optimization. If you charted + performance over time, it'd look like a + sawtooth, with a regular performance drop + during each optimization period. +

+ That's the dirty secret with all of this + stuff: "state of the art" models are + unprofitable due to high cost of inference + before optimization. After optimization they + still perform okay, but way below SOTA. It's + like a knife that's been sharpened until + razor sharp, then dulled shortly after. +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ girvo + 5 hours ago + + | + + | + + | + + [–] +
+
+
+
+ > If you charted performance over time, + it'd look like a sawtooth +

+ People have, though, and it doesn't show + that. I think it's more people getting hit + by the placebo effect, the novelty effect, + followed by the models by-definition + non-determinism leading people to say things + like "the model got worse". +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ gobdovan + 6 hours ago + + | + + | + + | + + | + + [–] +
+
+
+
+ Is this insider info? The 'charted + performance' caught my eye instantly. Couple + things I find odd tho: why sawtooth? it would + likely be square waves, as I'd imagine they + roll down the cost-saving version quite fast + per cohort. Also, aren't they unprofitable + either way? Why would they do it for + 'profitability'? +
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ bonoboTP + 5 hours ago + + | + + | + + | + + [–] +
+
+
+
+ It's rumors based on vibes. There are attempts + to track and quantify this with repeated model + evaluations multiple times per day, this but + no sawtooth pattern has emerged as far as I + know. +
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ chongli + 1 hour ago + + | + + | + + | + + [–] +
+
+
+
+ I don't want to go too far down the conspiracy + rabbit hole, but the vendors know everyone's + prompts so it would be trivial for them to + track the trackers and spoof the results. We + already know that they substitute different + models as a cost-saving measure, so + substituting models to fool the repeated + evaluations would be trivial. +

+ We also already know that they actively seek + out viral examples of poor performance on + certain prompts (e.g. counting Rs in + strawberry) and then monkey-patch them out + with targeted training. How can we be sure + they're not trying to spoof researchers who + are tracking model performance? Heck, they + might as well just call it "regression + testing." +

+

+ If their whole gig is an "emperor's new + clothes" bubble situation, then we can + expect them to try to uphold the masquerade + as long as possible. +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ chongli + 5 hours ago + + | + + | + + | + + | + + [–] +
+
+
+
+ It's not insider info, it's common knowledge + in the industry (Google model optimization). I + think they are unprofitable either way, but + unoptimized models burn runway a lot faster + than optimized ones. +

+ The reason it's not a square wave is because + new optimization techniques are always in + development, so you can't apply everything + immediately after training the new model. I + also think there's a marketing reason: if + the performance of a brand new model + declines rapidly after release then people + are going to notice much more readily than + with a gradual decline. The gradual decline + is thus engineered by applying different + optimizations gradually. +

+

+ It also has the side benefit that the future + next-gen model may be compared favourably + with the current-gen optimized (degraded) + model, setting up a rigged benchmark. If no + one has access to the original pre-optimized + current-gen model, no one can perform the + "proper" comparison to be able to gauge the + actual performance improvement. +

+

+ Lastly, I would point out that vendors like + OpenAI are already known to substitute + previous-gen models if they determine your + prompt is "simple." You should also count + this as a (rather crude) optimization + technique because it's going to degrade + performance any time your prompt is falsely + flagged as simple (false positive). +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ nextos + 5 hours ago + + | + + | + + | + + [–] +
+
+
+
+ You have a point but current LLM architectures + in particular are very fragile to data + poisoning [1,2]. +

+ [1] + https://www.anthropic.com/research/small-samples-poison +

+

+ [2] + https://arxiv.org/abs/2510.07192 +

+
+
+

+ reply +

+
+
+
+
+ + + + + + + + +
+ + +
+ ahazred8ta + 5 hours ago + + | + + | + + | + + [–] +
+
+
+
+ Yes, there are quite a few anti-AI projects. + https://old.reddit.com/r/badphilosophy/wiki/index +
+
+

+ reply +

+
+
+
+
+

+
+ + + + + + + +
+
+
+ Guidelines | + FAQ | Lists | + API | + Security | + Legal | + Apply to YC | + Contact

+
+ Search: + +
+
+
+
+ + + diff --git a/bin/unit_tests/assets/html/news.css b/bin/unit_tests/assets/html/news.css new file mode 100644 index 000000000..d85db99a2 --- /dev/null +++ b/bin/unit_tests/assets/html/news.css @@ -0,0 +1,176 @@ +body { font-family:Verdana, Geneva, sans-serif; font-size:10pt; color:#828282; } +td { font-family:Verdana, Geneva, sans-serif; font-size:10pt; color:#828282; } + +.admin td { font-family:Verdana, Geneva, sans-serif; font-size:8.5pt; color:#000000; } +.subtext td { font-family:Verdana, Geneva, sans-serif; font-size: 7pt; color:#828282; } + +input { font-family:monospace; font-size:10pt; } +input[type='submit'] { font-family:Verdana, Geneva, sans-serif; } +textarea { font-family:monospace; font-size:10pt; resize:both; } + +a:link { color:#000000; text-decoration:none; } +a:visited { color:#828282; text-decoration:none; } + +.default { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#828282; } +.admin { font-family:Verdana, Geneva, sans-serif; font-size:8.5pt; color:#000000; } +.title { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#828282; overflow:hidden; } +.subtext { font-family:Verdana, Geneva, sans-serif; font-size: 7pt; color:#828282; } +.yclinks { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } +.pagetop { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#222222; line-height:12px; } +.comhead { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } +.comment { font-family:Verdana, Geneva, sans-serif; font-size: 9pt; } +.hnname { margin-left:1px; margin-right: 5px; } + +#hnmain { min-width: 796px; } + +.title a { word-break: break-word; } + +.comment a:link, .comment a:visited { text-decoration: underline; } +.noshow { display: none; } +.nosee { visibility: hidden; pointer-events: none; cursor: default } + +.c00, .c00 a:link { color:#000000; } +.c5a, .c5a a:link, .c5a a:visited { color:#5a5a5a; } +.c73, .c73 a:link, .c73 a:visited { color:#737373; } +.c82, .c82 a:link, .c82 a:visited { color:#828282; } +.c88, .c88 a:link, .c88 a:visited { color:#888888; } +.c9c, .c9c a:link, .c9c a:visited { color:#9c9c9c; } +.cae, .cae a:link, .cae a:visited { color:#aeaeae; } +.cbe, .cbe a:link, .cbe a:visited { color:#bebebe; } +.cce, .cce a:link, .cce a:visited { color:#cecece; } +.cdd, .cdd a:link, .cdd a:visited { color:#dddddd; } + +.pagetop a:visited { color:#000000;} +.topsel a:link, .topsel a:visited { color:#ffffff; } + +.subtext a:link, .subtext a:visited { color:#828282; } +.subtext a:hover { text-decoration:underline; } + +.comhead a:link, .subtext a:visited { color:#828282; } +.comhead a:hover { text-decoration:underline; } + +.hnmore a:link, a:visited { color:#828282; } +.hnmore { text-decoration:underline; } + +.default p { margin-top: 8px; margin-bottom: 0px; } + +.pagebreak {page-break-before:always} + +pre { overflow: auto; padding: 2px; white-space: pre-wrap; overflow-wrap:anywhere; } +pre:hover { overflow:auto } + +.votearrow { + width: 10px; + height: 10px; + border: 0px; + margin: 3px 2px 6px; + background: url("triangle.svg"), linear-gradient(transparent, transparent) no-repeat; + background-size: 10px; +} + +.votelinks.nosee div.votearrow.rotate180 { + display: none; +} + +table.padtab td { padding:0px 10px } + +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { + .votearrow { + background-size: 10px; + background-image: url("triangle.svg"), linear-gradient(transparent, transparent); + } +} + +.rotate180 { + -webkit-transform: rotate(180deg); /* Chrome and other webkit browsers */ + -moz-transform: rotate(180deg); /* FF */ + -o-transform: rotate(180deg); /* Opera */ + -ms-transform: rotate(180deg); /* IE9 */ + transform: rotate(180deg); /* W3C complaint browsers */ + + /* IE8 and below */ + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=-1, M12=0, M21=0, M22=-1, DX=0, DY=0, SizingMethod='auto expand')"; +} + +/* mobile device */ +@media only screen +and (min-width : 300px) +and (max-width : 750px) { + #hnmain { width: 100%; min-width: 0; } + body { padding: 0; margin: 0; width: 100%; } + td { height: inherit !important; } + .title, .comment { font-size: inherit; } + span.pagetop { display: block; margin: 3px 5px; font-size: 12px; line-height: normal } + span.pagetop b { display: block; font-size: 15px; } + table.comment-tree .comment a { display: inline-block; max-width: 200px; overflow: hidden; white-space: nowrap; + text-overflow: ellipsis; vertical-align:top; } + img[src='s.gif'][width='40'] { width: 12px; } + img[src='s.gif'][width='80'] { width: 24px; } + img[src='s.gif'][width='120'] { width: 36px; } + img[src='s.gif'][width='160'] { width: 48px; } + img[src='s.gif'][width='200'] { width: 60px; } + img[src='s.gif'][width='240'] { width: 72px; } + img[src='s.gif'][width='280'] { width: 84px; } + img[src='s.gif'][width='320'] { width: 96px; } + img[src='s.gif'][width='360'] { width: 108px; } + img[src='s.gif'][width='400'] { width: 120px; } + img[src='s.gif'][width='440'] { width: 132px; } + img[src='s.gif'][width='480'] { width: 144px; } + img[src='s.gif'][width='520'] { width: 156px; } + img[src='s.gif'][width='560'] { width: 168px; } + img[src='s.gif'][width='600'] { width: 180px; } + img[src='s.gif'][width='640'] { width: 192px; } + img[src='s.gif'][width='680'] { width: 204px; } + img[src='s.gif'][width='720'] { width: 216px; } + img[src='s.gif'][width='760'] { width: 228px; } + img[src='s.gif'][width='800'] { width: 240px; } + img[src='s.gif'][width='840'] { width: 252px; } + .title { font-size: 11pt; line-height: 14pt; } + .subtext { font-size: 9pt; } + .votearrow { transform: scale(1.3,1.3); margin-right: 6px; } + .votearrow.rotate180 { + -webkit-transform: rotate(180deg) scale(1.3,1.3); /* Chrome and other webkit browsers */ + -moz-transform: rotate(180deg) scale(1.3,1.3); /* FF */ + -o-transform: rotate(180deg) scale(1.3,1.3); /* Opera */ + -ms-transform: rotate(180deg) scale(1.3,1.3); /* IE9 */ + transform: rotate(180deg) scale(1.3,1.3); /* W3C complaint browsers */ + } + .votelinks { min-width: 18px; } + .votelinks a { display: block; margin-bottom: 9px; } + input[type='text'], input[type='number'], textarea { font-size: 16px; width: 90%; } +} + +.comment { max-width: 1215px; overflow-wrap:anywhere; } + + + +@media only screen and (min-width : 300px) and (max-width : 389px) { + .comment { max-width: 270px; overflow: hidden } +} +@media only screen and (min-width : 390px) and (max-width : 509px) { + .comment { max-width: 350px; overflow: hidden } +} +@media only screen and (min-width : 510px) and (max-width : 599px) { + .comment { max-width: 460px; overflow: hidden } +} +@media only screen and (min-width : 600px) and (max-width : 689px) { + .comment { max-width: 540px; overflow: hidden } +} +@media only screen and (min-width : 690px) and (max-width : 809px) { + .comment { max-width: 620px; overflow: hidden } +} +@media only screen and (min-width : 810px) and (max-width : 899px) { + .comment { max-width: 730px; overflow: hidden } +} +@media only screen and (min-width : 900px) and (max-width : 1079px) { + .comment { max-width: 810px; overflow: hidden } +} +@media only screen and (min-width : 1080px) and (max-width : 1169px) { + .comment { max-width: 970px; overflow: hidden } +} +@media only screen and (min-width : 1170px) and (max-width : 1259px) { + .comment { max-width: 1050px; overflow: hidden } +} +@media only screen and (min-width : 1260px) and (max-width : 1349px) { + .comment { max-width: 1130px; overflow: hidden } +} diff --git a/bin/unit_tests/assets/html/triangle.svg b/bin/unit_tests/assets/html/triangle.svg new file mode 100644 index 000000000..6da385e37 --- /dev/null +++ b/bin/unit_tests/assets/html/triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/articles/cssspecification.md b/docs/articles/cssspecification.md index 9bd779d5e..abd20f914 100644 --- a/docs/articles/cssspecification.md +++ b/docs/articles/cssspecification.md @@ -906,7 +906,7 @@ Sets the hint font shadow offset. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside text or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UIPushButton (PushButton), EE::UI::UIToolti + EE::UI::UIPushButton (PushButton), EE::UI::UIToolti (Tooltip) * Data Type: [vector2-length](#vector2-length-data-type) * Default offset: `1dp 1dp` @@ -990,6 +990,19 @@ in which these items are displayed/sorted inside the button. --- +### input-mode + +Sets the input mode of the element. + +* Applicable to: EE::UI::UITextInput (TextInput) +* Data Type: [string-list](#string-list-data-type) +* Value List: + * `normal`: Normal text input. + * `password`: Password text input (bullets). +* Default value: `normal` + +--- + ### layout-gravity The layout gravity defines how the element gravitates against its parent (when possible). Gravity @@ -1742,8 +1755,7 @@ Sets the text selection background color on a text element that supports text se * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput - (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword) + (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList) * Data Type: [color](#color-data-type) * Default color: `#323232` @@ -1756,7 +1768,7 @@ Sets the text selection color on a text element that supports text selection. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside text or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UIPushButton (PushButton) + EE::UI::UIPushButton (PushButton) * Data Type: [color](#color-data-type) * Default color: `white` @@ -1918,8 +1930,7 @@ code implementation, but it's available as an option. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UITooltip (Tooltip), EE::UI::UITab (Tab), - EE::UI::UITextEdit (TextEdit) + EE::UI::UITooltip (Tooltip), EE::UI::UITab (Tab), EE::UI::UITextEdit (TextEdit) * Data Type: [string](#string-data-type) * Default value: _No value_ @@ -1954,8 +1965,7 @@ Enables/disables text selection in any element that contains text. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UITooltip (Tooltip), EE::UI::UITab (Tab), - EE::UI::UITextEdit (TextEdit) + EE::UI::UITooltip (Tooltip), EE::UI::UITab (Tab), EE::UI::UITextEdit (TextEdit) * Data Type: [boolean](#boolean-data-type) * Default value: `true` for TextEdit, TextInput. `false` for any other element. @@ -1968,8 +1978,7 @@ Sets the text shadow color. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside text or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UIPushButton (PushButton), EE::UI::UIToolti - (Tooltip) + EE::UI::UIPushButton (PushButton), EE::UI::UITooltip (Tooltip) * Data Type: [color](#color-data-type) * Default color: `#323232E6` @@ -1982,8 +1991,7 @@ Sets the text shadow offset. * Applicable to: EE::UI::UITextView (TextView) and any element that holds inside text or extends from a TextView. EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), EE::UI::UITextInput (TextInput), EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList), - EE::UI::UITextInputPassword (TextInputPassword), EE::UI::UIPushButton (PushButton), EE::UI::UIToolti - (Tooltip) + EE::UI::UIPushButton (PushButton), EE::UI::UITooltip (Tooltip) * Data Type: [vector2-length](#vector2-length-data-type) * Default offset: `1dp 1dp` @@ -2355,8 +2363,7 @@ Enables/disables word-wrap in the text view element. * Applicable to: EE::UI::UITextVIew (TextView), EE::UI::UITextInput (TextInput), EE::UI::UICheckBox (CheckBox), EE::UI::UIRadioButton (RadioButton), - EE::UI::UIListBoxItem (ListBox::item), EE::UI::UITextInputPassword (TextInputPassword), - EE::UI::UIDropDownList (DropDownList). + EE::UI::UIListBoxItem (ListBox::item), EE::UI::UIDropDownList (DropDownList). * Data Type: [boolean](#boolean-data-type) * Default value: `false` diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index ab677f21a..389a2228b 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -88,6 +88,15 @@ class EE_API String { return hash; } + static constexpr String::HashType hashToLower( const char* str, Int64 len ) { + String::HashType hash = 5381; + while ( --len >= 0 ) { + int c = *str++; + hash = ( ( hash << 5 ) + hash ) + ( c >= 'A' && c <= 'Z' ? c + 32 : c ); + } + return hash; + } + /** Escape string sequence */ static String escape( const String& str ); @@ -104,6 +113,18 @@ class EE_API String { * String( "text" ) ) */ static String::HashType hash( const String& str ); + /** @return string hash to lower. Assumes ASCII */ + static String::HashType hashToLower( const std::string& str ); + + /** @return string hash to lower. Assumes ASCII */ + static String::HashType hashToLower( const std::string_view& str ); + + /** @return string hash to lower. Assumes ASCII */ + static String::HashType hashToLower( const String::View& str ); + + /** @return string hash to lower. Assumes ASCII */ + static String::HashType hashToLower( const String& str ); + /** @return If the value passed is a character */ static bool isCharacter( const int& value ); @@ -312,6 +333,13 @@ class EE_API String { */ static bool startsWith( const String& haystack, const String& needle ); + /** Compare two strings from its beginning. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool startsWith( String::View haystack, String::View needle ); + /** Compare two strings from its beginning. * @param haystack The string to search in. * @param needle The searched string. @@ -326,6 +354,41 @@ class EE_API String { */ static bool startsWith( std::string_view haystack, std::string_view needle ); + /** Compare two strings from its beginning. Case-insensitive check. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool istartsWith( const std::string& haystack, const std::string& needle ); + + /** Compare two strings from its beginning. Case-insensitive check. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool istartsWith( const String& haystack, const String& needle ); + + /** Compare two strings from its beginning. Case-insensitive check. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool istartsWith( String::View haystack, String::View needle ); + + /** Compare two strings from its beginning. Case-insensitive check. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool istartsWith( const char* haystack, const char* needle ); + + /** Compare two strings from its beginning. Case-insensitive check. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool istartsWith( std::string_view haystack, std::string_view needle ); + /** Compare two strings from its end. * @param haystack The string to search in. * @param needle The searched string. @@ -340,6 +403,13 @@ class EE_API String { */ static bool endsWith( const String& haystack, const String& needle ); + /** Compare two strings from its end. + * @param haystack The string to search in. + * @param needle The searched string. + * @return true if string starts with the substring + */ + static bool endsWith( String::View haystack, String::View needle ); + /** @return True if a string contains a substring. * @param haystack The string to search in. * @param needle The searched string. @@ -382,7 +452,7 @@ class EE_API String { static int fuzzyMatch( const std::string& pattern, const std::string& string ); /** Replace all occurrences of the search string with the replacement string. */ - static void replaceAll( std::string& target, const std::string& that, const std::string& with ); + static void replaceAll( std::string& target, std::string_view that, std::string_view with ); /** Replace all occurrences of the search string with the replacement string. */ static void replaceAll( String& target, const String& that, const String& with ); diff --git a/include/eepp/graphics.hpp b/include/eepp/graphics.hpp index dcc20c8b5..ba8bdf8d0 100644 --- a/include/eepp/graphics.hpp +++ b/include/eepp/graphics.hpp @@ -58,6 +58,7 @@ #include #include #include +#include #include #include #include diff --git a/include/eepp/graphics/drawable.hpp b/include/eepp/graphics/drawable.hpp index 6ae137bc8..758618933 100644 --- a/include/eepp/graphics/drawable.hpp +++ b/include/eepp/graphics/drawable.hpp @@ -41,6 +41,10 @@ class EE_API Drawable { virtual Sizef getPixelsSize() = 0; + virtual Float getMinIntrinsicWidth() { return getPixelsSize().getWidth(); } + + virtual Float getMaxIntrinsicWidth() { return getPixelsSize().getWidth(); } + virtual void draw() = 0; virtual void draw( const Vector2f& position ) = 0; diff --git a/include/eepp/graphics/drawablesearcher.hpp b/include/eepp/graphics/drawablesearcher.hpp index 57d413e6f..81efb4da8 100644 --- a/include/eepp/graphics/drawablesearcher.hpp +++ b/include/eepp/graphics/drawablesearcher.hpp @@ -3,12 +3,14 @@ #include #include +#include namespace EE { namespace Graphics { class EE_API DrawableSearcher { public: - static Drawable* searchByName( const std::string& name, bool firstSearchSprite = false ); + static Drawable* searchByName( const std::string& name, bool firstSearchSprite = false, + Network::URI referer = "" ); static Drawable* searchById( const Uint32& id ); diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index 426299067..d803b5f63 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -69,9 +69,20 @@ class EE_API RichText : public Drawable { /** @return The maximum width for wrapping. */ Float getMaxWidth() const { return mMaxWidth; } + /** @return The minimum intrinsic width of the text block. */ + Float getMinIntrinsicWidth(); + + /** @return The maximum intrinsic width of the text block. */ + Float getMaxIntrinsicWidth(); + enum class BlockType { Text, Drawable, CustomSize }; - using Block = std::variant, std::shared_ptr, Sizef>; + struct CustomBlock { + Sizef size; + bool isBlock{ false }; + }; + + using Block = std::variant, std::shared_ptr, CustomBlock>; /** * @brief Adds a drawable (e.g., an image) into the text flow. @@ -82,8 +93,9 @@ class EE_API RichText : public Drawable { /** * @brief Adds a custom size spacer into the text flow. * @param size The physical dimensions of the spacer. + * @param isBlock Whether this spacer acts as a block-level element. */ - void addCustomSize( const Sizef& size ); + void addCustomSize( const Sizef& size, bool isBlock = false ); /** @return The list of blocks. */ const std::vector& getBlocks() { return mBlocks; } @@ -157,13 +169,18 @@ class EE_API RichText : public Drawable { Vector2f findCharacterPos( Int64 index ) const; /** @return A list of rectangles that cover the selection. */ - std::vector getSelectionRects() const; + SmallVector getSelectionRects() const; /** @return The current selection as a string. */ String getSelectionString() const; + /** Tries to update the layout if has been invalidated. This is automatically called before + * draw. */ void updateLayout(); + /** Invalidates the current layout */ + void invalidateLayout(); + protected: std::vector mBlocks; std::vector mLines; diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index c80ba0976..b832f861d 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -25,11 +26,6 @@ struct WhitespaceDisplayConfig { std::optional tabOffset; }; -struct TextSelectionRange { - Int64 start{ 0 }; - Int64 end{ 0 }; -}; - class EE_API Text { public: static bool TextShaperEnabled; @@ -126,14 +122,11 @@ class EE_API Text { TextDirection direction = TextDirection::Unspecified, const Vector2f& initialOffset = {} ); - static Vector2f findCharacterPos( std::size_t index, Font* font, const Uint32& fontSize, - const String& string, const Uint32& style, - const Uint32& tabWidth = 4, - const Float& outlineThickness = 0.f, - std::optional tabOffset = {}, bool allowNewLine = true, - Uint32 textHints = 0, - TextDirection direction = TextDirection::Unspecified, - const Vector2f& initialOffset = {} ); + static Vector2f findCharacterPos( + std::size_t index, Font* font, const Uint32& fontSize, const String& string, + const Uint32& style, const Uint32& tabWidth = 4, const Float& outlineThickness = 0.f, + std::optional tabOffset = {}, bool allowNewLine = true, Uint32 textHints = 0, + TextDirection direction = TextDirection::Unspecified, const Vector2f& initialOffset = {} ); static std::size_t findLastCharPosWithinLength( Font* font, const Uint32& fontSize, const String& string, @@ -250,7 +243,7 @@ class EE_API Text { Float getTextHeight(); /** @return The line spacing */ - Float getLineSpacing(); + Float getLineSpacing() const; /** Draw the cached text on screen */ void draw( const Float& X, const Float& Y, const Vector2f& scale = Vector2f::One, @@ -390,7 +383,7 @@ class EE_API Text { /** @return A list of rectangles that cover the selection of the string, each rectangle * has the line spacing height and covers the width of the selection. */ - std::vector getSelectionRects( TextSelectionRange range ); + SmallVector getSelectionRects( TextSelectionRange range ); protected: struct VertexCoords { @@ -507,6 +500,8 @@ class EE_API Text { std::optional tabOffset, Uint32 textHints, TextDirection direction, LineWrapMode lineWrapMode, Float maxWrapWidth, const Vector2f& initialOffset = {} ); + + void checkColorEmojis(); }; }} // namespace EE::Graphics diff --git a/include/eepp/graphics/textlayout.hpp b/include/eepp/graphics/textlayout.hpp index 5d878f36f..a52986342 100644 --- a/include/eepp/graphics/textlayout.hpp +++ b/include/eepp/graphics/textlayout.hpp @@ -48,6 +48,7 @@ class EE_API TextLayout { LineWrapMode lineWrapMode = LineWrapMode::NoWrap, Uint32 wrapWidth = 0, bool keepIndentation = false, Float initialXOffset = 0 ); + static void clearLayoutCache(); protected: static void wrapLayout( const String::View& string, TextLayout&, LineWrapMode lineWrapMode, Float wrapWidth, Float vspace, bool keepIndentation, Font* font, diff --git a/include/eepp/graphics/textselectionrange.hpp b/include/eepp/graphics/textselectionrange.hpp new file mode 100644 index 000000000..31917c944 --- /dev/null +++ b/include/eepp/graphics/textselectionrange.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace EE::Graphics { + +struct TextSelectionRange { + Int64 start{ 0 }; + Int64 end{ 0 }; +}; + +} // namespace EE::Graphics diff --git a/include/eepp/network/http.hpp b/include/eepp/network/http.hpp index 49caaa640..246b7b564 100644 --- a/include/eepp/network/http.hpp +++ b/include/eepp/network/http.hpp @@ -52,6 +52,9 @@ class EE_API Http : NonCopyable { NotModified = 304, ///< For conditional requests, means the requested page hasn't ///< changed and doesn't need to be refreshed + TemporaryRedirect = 307, ///< The requested page has temporarily moved to a new location + PermanentRedirect = 308, ///< The requested page has permanently moved to a new location + // 4xx: client error BadRequest = 400, ///< The server couldn't understand the request (syntax error) Unauthorized = 401, ///< The requested page needs an authentication to be accessed @@ -112,6 +115,9 @@ class EE_API Http : NonCopyable { ** @return Status code of the response */ Status getStatus() const; + /** @return True if the response status is successful (2XX status) */ + bool isOK() const; + /** @brief Get the response status description */ const char* getStatusDescription() const; @@ -334,7 +340,7 @@ class EE_API Http : NonCopyable { const CancelCallback& getCancelCallback() const; /** Cancels the current request if being processed */ - void cancel(); + void cancel( bool resetCancelCallback = false ); /** @return True if the current request was cancelled */ const bool& isCancelled() const; @@ -396,6 +402,10 @@ class EE_API Http : NonCopyable { URI mProxy; ///< Proxy information }; + static void setDefaultUserAgent( const std::string& userAgent ); + + static std::string getDefaultUserAgent(); + /** @brief Default constructor */ Http(); @@ -536,7 +546,7 @@ class EE_API Http : NonCopyable { bool isProxied() const; /** @return If request has been found and canceled */ - bool setCancelRequest( Uint64 reqId ); + bool setCancelRequest( Uint64 reqId, bool resetCancelCallback = false ); /** Helper class to build the body of a multipart/form-data request. */ class EE_API MultipartEntitiesBuilder { @@ -711,7 +721,7 @@ class EE_API Http : NonCopyable { Uint64 id() const { return mId; } - void cancel(); + void cancel( bool resetCancelCallback = false ); protected: friend class Http; diff --git a/include/eepp/network/uri.hpp b/include/eepp/network/uri.hpp index 36348e506..ef8bce336 100644 --- a/include/eepp/network/uri.hpp +++ b/include/eepp/network/uri.hpp @@ -179,7 +179,7 @@ class EE_API URI { const std::string& getFragment() const; /** Sets the fragment part of the URI. */ - void getFragment( const std::string& fragment ); + void setFragment( const std::string& fragment ); /** Sets the path, query and fragment parts of the URI. */ void setPathEtc( const std::string& pathEtc ); diff --git a/include/eepp/scene/keyevent.hpp b/include/eepp/scene/keyevent.hpp index bde81c471..cbdff3892 100644 --- a/include/eepp/scene/keyevent.hpp +++ b/include/eepp/scene/keyevent.hpp @@ -7,6 +7,10 @@ using namespace EE::Window; +namespace EE::Window { +class Input; +} + namespace EE { namespace Scene { class EE_API KeyEvent : public Event { @@ -38,6 +42,10 @@ class EE_API KeyEvent : public Event { class EE_API TextInputEvent : public Event { public: + /* This verification checks if the user is not pressing any key modifier that should invalidate + * the text input event. */ + static bool isValidTextInputEvent( Input* input, const TextInputEvent& event ); + TextInputEvent( Node* node, const Uint32& eventNum, const Uint32& chr, const Uint32& timestamp ); @@ -47,6 +55,9 @@ class EE_API TextInputEvent : public Event { String getText() const; + /* @see isValidTextInputEvent */ + bool isValid( Input* input ) const; + protected: String::StringBaseType mChar; Uint32 mTimestamp; diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index aa462f250..020fbe14b 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -1890,6 +1890,28 @@ class EE_API Node : public Transformable { */ bool hasEventsOfType( const Uint32& eventType ) const; + /** + * @brief Enables clipping for the node's bounds. + * + * Convenience overload that automatically determines if clipping planes + * are needed based on transforms. + * + * @param x Left edge. + * @param y Top edge. + * @param Width Width. + * @param Height Height. + */ + void clipSmartEnable( const Int32& x, const Int32& y, const Uint32& Width, + const Uint32& Height ); + + /** + * @brief Disables clipping. + * + * Convenience overload that automatically determines if clipping planes + * are active. + */ + void clipSmartDisable(); + protected: /** @brief Map of event type to callback ID to callback function. */ typedef UnorderedMap> EventsMap; @@ -2465,28 +2487,6 @@ class EE_API Node : public Transformable { */ void clipSmartDisable( bool needsClipPlanes ); - /** - * @brief Enables clipping for the node's bounds. - * - * Convenience overload that automatically determines if clipping planes - * are needed based on transforms. - * - * @param x Left edge. - * @param y Top edge. - * @param Width Width. - * @param Height Height. - */ - void clipSmartEnable( const Int32& x, const Int32& y, const Uint32& Width, - const Uint32& Height ); - - /** - * @brief Disables clipping. - * - * Convenience overload that automatically determines if clipping planes - * are active. - */ - void clipSmartDisable(); - /** * @brief Finds the nearest draw invalidator in the parent chain. * diff --git a/include/eepp/scene/scenemanager.hpp b/include/eepp/scene/scenemanager.hpp index bdb02de4b..68471dcc0 100644 --- a/include/eepp/scene/scenemanager.hpp +++ b/include/eepp/scene/scenemanager.hpp @@ -37,8 +37,6 @@ class EE_API SceneManager { void update(); - bool isShuttingDown() const; - UISceneNode* getUISceneNode(); void setCurrentUISceneNode( UISceneNode* uiSceneNode ); @@ -48,7 +46,6 @@ class EE_API SceneManager { protected: Clock mClock; UISceneNode* mUISceneNode; - bool mIsShuttingDown; std::vector mSceneNodes; }; diff --git a/include/eepp/system/color.hpp b/include/eepp/system/color.hpp index a304923b9..b4e2c97fb 100644 --- a/include/eepp/system/color.hpp +++ b/include/eepp/system/color.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -143,6 +144,18 @@ template class tColor { } tRGB toRGB() { return tRGB( r, g, b ); } + + /** + * @brief Calculates the perceived luminance (Luma) of the color. + * Uses the Rec. 601 luma formula: L = 0.299*R + 0.587*G + 0.114*B. + * Perceived luminance aligns with human vision, making it ideal for UI contrast. + * @note Note on HSL (Colorf): If this object is being used to store HSL values + * (for instance, a Colorf returned by Color::toHsl()), do NOT call this method + * directly. Perceived luminance mathematically requires RGB components. + * You must either call this on the original RGB object, or convert the HSL + * Colorf back to RGB first (e.g., using Color::fromHsl(hslColor)). + */ + T perceivedLuminance() const { return static_cast( 0.299f * r + 0.587f * g + 0.114f * b ); } }; typedef tColor ColorAf; @@ -208,13 +221,17 @@ class EE_API Color : public tColor { static Color fromString( std::string str ); - static bool isColorString( std::string str ); + static bool isColorString( std::string_view str, bool searchColorNames = true ); + + static bool isColorString( String::View str, bool searchColorNames = true ); static void registerColor( const std::string& name, const Color& color ); static bool unregisterColor( const std::string& name ); - static bool validHexColorString( const std::string& hexColor ); + static bool validHexColorString( std::string_view hexColor ); + + static bool validHexColorString( String::View hexColor ); static const Color Transparent; static const Color Black; @@ -384,9 +401,13 @@ class EE_API Color : public tColor { private: static UnorderedMap sColors; + static UnorderedMap sColorHash; static UnorderedMap sColorMap; static void initColorMap(); + + template + static bool isColorStringT( StringType str, bool searchColorNames ); }; typedef Color ColorA; diff --git a/include/eepp/system/compression.hpp b/include/eepp/system/compression.hpp index bec901550..5b331b5c6 100644 --- a/include/eepp/system/compression.hpp +++ b/include/eepp/system/compression.hpp @@ -9,7 +9,7 @@ namespace EE { namespace System { class EE_API Compression { public: - enum Mode { MODE_DEFLATE, MODE_GZIP }; + enum Mode { MODE_DEFLATE, MODE_GZIP, MODE_BROTLI }; enum Status { OK = 0, @@ -29,10 +29,16 @@ class EE_API Compression { int level = -1; }; + struct BrotliConfig { + int quality = -1; + int windowBits = -1; + }; + struct Config { Config() {} ZlibConfig zlib; GzipConfig gzip; + BrotliConfig brotli; }; static Status compress( Uint8* dst, Uint64 dstMaxSize, const Uint8* src, Uint64 srcSize, diff --git a/include/eepp/system/filesystem.hpp b/include/eepp/system/filesystem.hpp index c3c9520a9..e4a1c5e46 100644 --- a/include/eepp/system/filesystem.hpp +++ b/include/eepp/system/filesystem.hpp @@ -91,6 +91,12 @@ class EE_API FileSystem { /** @return The modification date of the file */ static Uint32 fileGetModificationDate( const std::string& filepath ); + /** @return The number of lines in a file. + * @param path The file path. + * @param isBinary If provided, it will be set to true if the file is binary. + */ + static size_t fileCountLines( const std::string& path, bool* isBinary = nullptr ); + /** @return If a file path is writeable */ static bool fileCanWrite( const std::string& filepath ); diff --git a/include/eepp/system/functionstring.hpp b/include/eepp/system/functionstring.hpp index 4e2300baf..3ce0f169e 100644 --- a/include/eepp/system/functionstring.hpp +++ b/include/eepp/system/functionstring.hpp @@ -2,21 +2,36 @@ #define EE_SYSTEM_FUNCTIONSTRING_HPP #include +#include +#include + +#include #include -#include + +template +concept AllowedFunctionString = + std::same_as || std::same_as; namespace EE { namespace System { class EE_API FunctionString { public: - static FunctionString parse( const std::string& function ); + using Parameters = SmallVector; + using TypeStringVector = SmallVector; - FunctionString( const std::string& name, const std::vector& parameters, - const std::vector& typeStringData ); + static FunctionString parse( std::string_view function ); + + static FunctionString parse( String::View function ); + + FunctionString( const std::string& name, const Parameters& parameters, + const TypeStringVector& typeStringData ); + + FunctionString( const std::string& name, Parameters&& parameters, + TypeStringVector&& typeStringData ); const std::string& getName() const; - const std::vector& getParameters() const; + const Parameters& getParameters() const; bool parameterWasString( Uint32 index ) const; @@ -24,8 +39,10 @@ class EE_API FunctionString { protected: std::string name; - std::vector parameters; - std::vector typeStringData; + Parameters parameters; + TypeStringVector typeStringData; + + template static FunctionString parse( StringType function ); }; }} // namespace EE::System diff --git a/include/eepp/system/iostreamdeflate.hpp b/include/eepp/system/iostreamdeflate.hpp index ffc397dc0..07d598bd6 100644 --- a/include/eepp/system/iostreamdeflate.hpp +++ b/include/eepp/system/iostreamdeflate.hpp @@ -7,7 +7,7 @@ namespace EE { namespace System { -struct LocalStreamData; +struct LocalDeflateStreamData; /** @brief Implementation of a deflating stream */ class EE_API IOStreamDeflate : public IOStream { @@ -44,7 +44,7 @@ class EE_API IOStreamDeflate : public IOStream { IOStream& mStream; Compression::Mode mMode; ScopedBuffer mBuffer; - LocalStreamData* mLocalStream; + LocalDeflateStreamData* mLocalStream; }; }} // namespace EE::System diff --git a/include/eepp/system/iostreaminflate.hpp b/include/eepp/system/iostreaminflate.hpp index 1adc57cf3..62fa2b2c5 100644 --- a/include/eepp/system/iostreaminflate.hpp +++ b/include/eepp/system/iostreaminflate.hpp @@ -7,7 +7,7 @@ namespace EE { namespace System { -struct LocalStreamData; +struct LocalInflateStreamData; /** @brief Implementation of a inflating stream */ class EE_API IOStreamInflate : public IOStream { @@ -41,7 +41,7 @@ class EE_API IOStreamInflate : public IOStream { IOStream& mStream; Compression::Mode mMode; ScopedBuffer mBuffer; - LocalStreamData* mLocalStream; + LocalInflateStreamData* mLocalStream; }; }} // namespace EE::System diff --git a/include/eepp/system/luapattern.hpp b/include/eepp/system/luapattern.hpp index 9aca1dd3e..fb70b2009 100644 --- a/include/eepp/system/luapattern.hpp +++ b/include/eepp/system/luapattern.hpp @@ -15,13 +15,13 @@ class EE_API LuaPattern : public PatternMatcher { static std::string_view getURIPattern(); static std::string matchesAny( const std::vector& stringvec, - const std::string_view& pattern ); + std::string_view pattern ); - static std::string match( const std::string& string, const std::string_view& pattern ); + static std::string match( std::string_view string, std::string_view pattern ); - static Range firstMatch( const std::string& string, const std::string_view& pattern ); + static Range firstMatch( std::string_view string, std::string_view pattern ); - static bool hasMatches( const std::string& string, const std::string_view& pattern ); + static bool hasMatches( std::string_view string, std::string_view pattern ); LuaPattern( std::string_view pattern, Uint32 options = Options::None ); diff --git a/include/eepp/system/resourcemanager.hpp b/include/eepp/system/resourcemanager.hpp index 43dd7b639..cd72f795d 100644 --- a/include/eepp/system/resourcemanager.hpp +++ b/include/eepp/system/resourcemanager.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -81,16 +83,14 @@ template class ResourceManager { pred( res ); } - template - T* findIf( Predicate pred ) const { + template T* findIf( Predicate pred ) const { for ( const auto& res : mResources ) if ( pred( res ) ) return res.second; return nullptr; } - template - T* findIf( Predicate pred ) { + template T* findIf( Predicate pred ) { for ( auto& res : mResources ) if ( pred( res ) ) return res.second; @@ -98,6 +98,7 @@ template class ResourceManager { } protected: + Mutex mMutex; UnorderedMap mResources; bool mIsDestroying; }; @@ -115,16 +116,19 @@ template ResourceManager::~ResourceManager() { template void ResourceManager::destroy() { mIsDestroying = true; - for ( auto& it : mResources ) { - T* res = it.second; - eeSAFE_DELETE( res ); + { + Lock l( mMutex ); + for ( auto& it : mResources ) { + T* res = it.second; + eeSAFE_DELETE( res ); + } + mResources.clear(); } - mResources.clear(); - mIsDestroying = false; } +// This is not thread safe template UnorderedMap& ResourceManager::getResources() { return mResources; } @@ -132,6 +136,7 @@ template UnorderedMap& ResourceManager::getRe template T* ResourceManager::add( T* resource ) { if ( NULL != resource ) { if ( !existsId( resource->getId() ) ) { + Lock l( mMutex ); mResources[resource->getId()] = resource; return resource; @@ -147,6 +152,7 @@ template T* ResourceManager::add( T* resource ) { return add( resource ); } + Lock l( mMutex ); mResources[resource->getId()] = resource; return resource; } @@ -155,8 +161,11 @@ template T* ResourceManager::add( T* resource ) { template bool ResourceManager::remove( T* resource, bool remove ) { if ( NULL != resource ) { - mResources.erase( resource->getId() ); + { + Lock l( mMutex ); + mResources.erase( resource->getId() ); + } if ( remove ) eeSAFE_DELETE( resource ); @@ -179,6 +188,7 @@ template bool ResourceManager::exists( const std::string& name ) { } template bool ResourceManager::existsId( const String::HashType& id ) { + Lock l( mMutex ); return mResources.find( id ) != mResources.end(); } @@ -187,17 +197,20 @@ template T* ResourceManager::getByName( const std::string& name ) { } template T* ResourceManager::getById( const String::HashType& id ) { + Lock l( mMutex ); auto it = mResources.find( id ); return it != mResources.end() ? it->second : nullptr; } template void ResourceManager::printNames() { + Lock l( mMutex ); for ( auto& it : mResources ) { eePRINTL( "'%s'", it.second->getName().c_str() ); } } template Uint32 ResourceManager::getCount() { + Lock l( mMutex ); return (Uint32)mResources.size(); } @@ -274,6 +287,7 @@ template class ResourceManagerMulti { const bool& isDestroying() const; protected: + Mutex mMutex; std::unordered_multimap mResources; bool mIsDestroying; }; @@ -291,12 +305,15 @@ template ResourceManagerMulti::~ResourceManagerMulti() { template void ResourceManagerMulti::destroy() { mIsDestroying = true; - for ( auto& it : mResources ) { - T* res = it.second; - eeSAFE_DELETE( res ); - } + { + Lock l( mMutex ); + for ( auto& it : mResources ) { + T* res = it.second; + eeSAFE_DELETE( res ); + } - mResources.clear(); + mResources.clear(); + } mIsDestroying = false; } @@ -308,6 +325,7 @@ std::unordered_multimap& ResourceManagerMulti::getResou template T* ResourceManagerMulti::add( T* resource ) { if ( NULL != resource ) { + Lock l( mMutex ); mResources.insert( std::pair( resource->getId(), resource ) ); return resource; } @@ -316,14 +334,17 @@ template T* ResourceManagerMulti::add( T* resource ) { template bool ResourceManagerMulti::remove( T* resource, bool remove ) { if ( NULL != resource ) { - auto range = mResources.equal_range( resource->getId() ); - auto it = range.first; - while ( it != range.second ) { - if ( it->second == resource ) { - mResources.erase( it ); - break; + { + Lock l( mMutex ); + auto range = mResources.equal_range( resource->getId() ); + auto it = range.first; + while ( it != range.second ) { + if ( it->second == resource ) { + mResources.erase( it ); + break; + } + it++; } - it++; } if ( remove ) @@ -350,6 +371,7 @@ template bool ResourceManagerMulti::exists( const std::string& name } template bool ResourceManagerMulti::existsId( const String::HashType& id ) { + Lock l( mMutex ); return mResources.find( id ) != mResources.end(); } @@ -358,21 +380,25 @@ template T* ResourceManagerMulti::getByName( const std::string& nam } template T* ResourceManagerMulti::getById( const String::HashType& id ) { + Lock l( mMutex ); auto it = mResources.find( id ); return it != mResources.end() ? it->second : nullptr; } template void ResourceManagerMulti::printNames() { + Lock l( mMutex ); for ( auto& it : mResources ) { eePRINTL( "'%s'", it.second->getName().c_str() ); } } template Uint32 ResourceManagerMulti::getCount() { + Lock l( mMutex ); return (Uint32)mResources.size(); } template Uint32 ResourceManagerMulti::getCount( const String::HashType& id ) { + Lock l( mMutex ); return mResources.count( id ); } diff --git a/include/eepp/system/singleton.hpp b/include/eepp/system/singleton.hpp index c450f78da..663ec457b 100644 --- a/include/eepp/system/singleton.hpp +++ b/include/eepp/system/singleton.hpp @@ -13,6 +13,8 @@ static T* createSingleton(); \ \ static T* existsSingleton(); \ + \ + static bool isShuttingDown(); \ \ static T* instance(); \ \ @@ -23,6 +25,7 @@ #define SINGLETON_DECLARE_IMPLEMENTATION( T ) \ \ static T* ms_singleton = NULL; \ + static bool ms_is_shutting_down = false; \ static Mutex ms_mutex; \ \ T* T::createSingleton() { \ @@ -34,6 +37,10 @@ \ T* T::existsSingleton() { \ return ms_singleton; \ + } \ + \ + bool T::isShuttingDown() { \ + return ms_is_shutting_down; \ } \ \ T* T::instance() { \ @@ -41,8 +48,10 @@ } \ \ void T::destroySingleton() { \ + ms_is_shutting_down = true; \ Lock l( ms_mutex ); \ eeSAFE_DELETE( ms_singleton ); \ + ms_is_shutting_down = false; \ } \ \ void T::detachSingleton() { \ diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index f75cf19eb..9ef72a46a 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -45,6 +45,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -67,6 +70,7 @@ #include #include #include +#include #include #include #include @@ -79,7 +83,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -110,6 +116,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +148,6 @@ #include #include #include -#include #include #include #include diff --git a/include/eepp/ui/abstract/uiabstracttableview.hpp b/include/eepp/ui/abstract/uiabstracttableview.hpp index 4df910bc2..4c06f52ee 100644 --- a/include/eepp/ui/abstract/uiabstracttableview.hpp +++ b/include/eepp/ui/abstract/uiabstracttableview.hpp @@ -42,6 +42,9 @@ class EE_API UIAbstractTableView : public UIAbstractView { virtual void selectAll(); + virtual std::vector getSelectionRange( const ModelIndex& start, + const ModelIndex& end ) const; + const Float& getDragBorderDistance() const; void setDragBorderDistance( const Float& dragBorderDistance ); diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 93cdd129e..6cbff682e 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -231,6 +231,14 @@ enum class PropertyId : Uint32 { MenuWidthMode = String::hash( "menu-width-mode" ), ExpandText = String::hash( "expand-text" ), Colspan = String::hash( "colspan" ), + TableLayout = String::hash( "table-layout" ), + Cellpadding = String::hash( "cellpadding" ), + Cellspacing = String::hash( "cellspacing" ), + Size = String::hash( "size" ), + Type = String::hash( "type" ), + Rows = String::hash( "rows" ), + Cols = String::hash( "cols" ), + InputMode = String::hash( "input-mode" ), }; enum class PropertyType : Uint32 { diff --git a/include/eepp/ui/css/propertyspecification.hpp b/include/eepp/ui/css/propertyspecification.hpp index 30c969bf3..06edae2c9 100644 --- a/include/eepp/ui/css/propertyspecification.hpp +++ b/include/eepp/ui/css/propertyspecification.hpp @@ -15,6 +15,8 @@ class EE_API PropertySpecification { PropertyDefinition& registerProperty( const std::string& propertyVame, const std::string& defaultValue, bool inherited ); + const PropertyDefinition* getProperty( const PropertyId& id ) const; + const PropertyDefinition* getProperty( const Uint32& id ) const; const PropertyDefinition* getProperty( const std::string& name ) const; diff --git a/include/eepp/ui/css/stylesheet.hpp b/include/eepp/ui/css/stylesheet.hpp index 1b39cb216..db617974f 100644 --- a/include/eepp/ui/css/stylesheet.hpp +++ b/include/eepp/ui/css/stylesheet.hpp @@ -58,6 +58,8 @@ class EE_API StyleSheet { void removeAllWithMarker( const Uint32& marker ); + void removeAllWithoutMarker( const Uint32& marker ); + bool markerExists( const Uint32& marker ) const; StyleSheet getAllWithMarker( const Uint32& marker ) const; diff --git a/include/eepp/ui/css/stylesheetlength.hpp b/include/eepp/ui/css/stylesheetlength.hpp index 028f6be30..a1495a840 100644 --- a/include/eepp/ui/css/stylesheetlength.hpp +++ b/include/eepp/ui/css/stylesheetlength.hpp @@ -33,6 +33,7 @@ class EE_API StyleSheetLength { Dprd, Dpru, Dpr, + Ch, }; static Unit unitFromString( std::string unitStr ); @@ -41,7 +42,8 @@ class EE_API StyleSheetLength { static bool isLength( const std::string& unitStr ); - static StyleSheetLength fromString( const std::string& str, const Float& defaultValue = 0 ); + static StyleSheetLength fromString( const std::string& str, const Float& defaultValue = 0, + bool pxAsDp = false ); StyleSheetLength(); diff --git a/include/eepp/ui/css/stylesheetproperty.hpp b/include/eepp/ui/css/stylesheetproperty.hpp index e45f8b458..2567404d0 100644 --- a/include/eepp/ui/css/stylesheetproperty.hpp +++ b/include/eepp/ui/css/stylesheetproperty.hpp @@ -125,7 +125,7 @@ class EE_API StyleSheetProperty { Rect asRect( const Rect& defaultValue = Rect() ) const; - Rectf asRectf( const Rectf& defaultValue = Rectf() ) const; + Rectf asRectf( const Rectf& defaultValue = Rectf::Zero ) const; Uint32 asTextDecoration() const; @@ -186,6 +186,8 @@ class EE_API StyleSheetProperty { bool isCachedProperty() const; + void setImportant( bool important ); + protected: std::string mName; String::HashType mNameHash; diff --git a/include/eepp/ui/css/stylesheetselectorrule.hpp b/include/eepp/ui/css/stylesheetselectorrule.hpp index e188e2db9..684fcb882 100644 --- a/include/eepp/ui/css/stylesheetselectorrule.hpp +++ b/include/eepp/ui/css/stylesheetselectorrule.hpp @@ -21,9 +21,11 @@ class EE_API StyleSheetSelectorRule { Pressed = ( 1 << 3 ), Disabled = ( 1 << 4 ), FocusWithin = ( 1 << 5 ), + Link = ( 1 << 6 ), + Visited = ( 1 << 7 ), }; - static constexpr auto PseudoClassesTotal = 6; + static constexpr auto PseudoClassesTotal = 8; enum TypeIdentifier { TAG = 0, diff --git a/include/eepp/ui/css/stylesheetspecification.hpp b/include/eepp/ui/css/stylesheetspecification.hpp index 3d9f3dde3..e52540971 100644 --- a/include/eepp/ui/css/stylesheetspecification.hpp +++ b/include/eepp/ui/css/stylesheetspecification.hpp @@ -40,6 +40,8 @@ class EE_API StyleSheetSpecification { PropertyDefinition& registerProperty( const std::string& propertyVame, const std::string& defaultValue, bool inherited = false ); + const PropertyDefinition* getProperty( const PropertyId& id ) const; + const PropertyDefinition* getProperty( const Uint32& id ) const; const PropertyDefinition* getProperty( const std::string& name ) const; diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index ea1f86ffb..b74c51f49 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -48,6 +48,8 @@ class EE_API TextDocument { enum class IndentType { IndentSpaces, IndentTabs }; + enum class AutoIndentConfig { None, Preserve, Smart }; + enum class FindReplaceType { Normal, LuaPattern, RegEx }; enum class LoadStatus { Loaded, Interrupted, Failed }; @@ -477,6 +479,10 @@ class EE_API TextDocument { void setIndentType( const IndentType& indentType ); + const AutoIndentConfig& getAutoIndent() const; + + void setAutoIndent( const AutoIndentConfig& autoIndent ); + const SyntaxDefinition& getSyntaxDefinition() const; void setSyntaxDefinition( std::shared_ptr definition ); @@ -620,6 +626,9 @@ class EE_API TextDocument { TextRange addSelections( TextRanges&& selections ); + /* @return returns the selection index otherwise -1 */ + int selectionIndex( TextRange selection ) const; + void popSelection(); bool deleteSelection( const size_t& cursorIdx ); @@ -733,6 +742,12 @@ class EE_API TextDocument { void convertIndentationToSpaces(); + void clearIndentation(); + + String toString(); + + std::string toUtf8String(); + protected: friend class TextUndoStack; friend class FoldRangeService; @@ -774,6 +789,7 @@ class EE_API TextDocument { std::vector> mAutoCloseBracketsPairs; Uint32 mIndentWidth{ 4 }; IndentType mIndentType{ IndentType::IndentTabs }; + AutoIndentConfig mAutoIndent{ AutoIndentConfig::Smart }; Clock mTimer; std::shared_ptr mSyntaxDefinition; std::string mDefaultFileName; @@ -867,7 +883,6 @@ class EE_API TextDocument { TextPosition findPreviousEmptyLines( size_t selIdx ); TextPosition findNextEmptyLines( size_t selIdx ); - }; struct TextSearchParams { diff --git a/include/eepp/ui/doc/textrange.hpp b/include/eepp/ui/doc/textrange.hpp index eb6b2cd29..33b2610a7 100644 --- a/include/eepp/ui/doc/textrange.hpp +++ b/include/eepp/ui/doc/textrange.hpp @@ -2,8 +2,11 @@ #define EE_UI_DOC_TEXTRANGE_HPP #include +#include #include +using namespace EE::Graphics; + namespace EE { namespace UI { namespace Doc { class EE_API TextRange { @@ -120,7 +123,12 @@ class EE_API TextRange { static TextRange convertToLineColumn( const std::string_view& text, Int64 startOffset, Int64 endOffset ); - Int64 minimumDistance(const TextRange& other) const; + Int64 minimumDistance( const TextRange& other ) const; + + static TextSelectionRange convertToOffset( const String::View& text, const TextRange& range ); + + static TextSelectionRange convertToOffset( const std::string_view& text, + const TextRange& range ); private: TextPosition mStart; @@ -133,6 +141,9 @@ class EE_API TextRange { template static TextRange convertToLineColumn( const StringType& text, Int64 startOffset, Int64 endOffset ); + + template + static TextSelectionRange convertToOffset( const StringType& text, const TextRange& range ); }; class EE_API TextRanges : public std::vector { diff --git a/include/eepp/ui/htmlinput.hpp b/include/eepp/ui/htmlinput.hpp new file mode 100644 index 000000000..cc2520ee1 --- /dev/null +++ b/include/eepp/ui/htmlinput.hpp @@ -0,0 +1,47 @@ +#ifndef EE_UI_HTMLINPUT_HPP +#define EE_UI_HTMLINPUT_HPP + +#include + +namespace EE { namespace UI { + +class EE_API HTMLInput : public UIWidget { + public: + static HTMLInput* New(); + + HTMLInput(); + + virtual Uint32 getType() const; + + virtual bool isType( const Uint32& type ) const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + + virtual std::vector getPropertiesImplemented() const; + + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + + const std::string& getInputType() const; + + void setInputType( const std::string& type ); + + UIWidget* getChildWidget() const; + + protected: + std::string mInputType{ "text" }; + UIWidget* mChildWidget{ nullptr }; + std::map mProperties; + + void createChildWidget(); + + virtual void onSizeChange(); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/htmltextarea.hpp b/include/eepp/ui/htmltextarea.hpp new file mode 100644 index 000000000..ac4f65d2b --- /dev/null +++ b/include/eepp/ui/htmltextarea.hpp @@ -0,0 +1,51 @@ +#ifndef EE_UI_HTMLTEXTAREA_HPP +#define EE_UI_HTMLTEXTAREA_HPP + +#include + +namespace EE { namespace UI { + +class EE_API HTMLTextArea : public UITextEdit { + public: + static HTMLTextArea* New(); + + HTMLTextArea(); + + virtual Uint32 getType() const; + + virtual bool isType( const Uint32& type ) const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + + virtual std::vector getPropertiesImplemented() const; + + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + + virtual Float getMinIntrinsicHeight() const; + + virtual Float getMaxIntrinsicHeight() const; + + Uint32 getRows() const; + + void setRows( Uint32 rows ); + + Uint32 getCols() const; + + void setCols( Uint32 cols ); + + protected: + Uint32 mRows{ 2 }; + Uint32 mCols{ 20 }; + bool mPacking{ false }; + + virtual void onAutoSize(); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/htmltextinput.hpp b/include/eepp/ui/htmltextinput.hpp new file mode 100644 index 000000000..247ed9da0 --- /dev/null +++ b/include/eepp/ui/htmltextinput.hpp @@ -0,0 +1,44 @@ +#ifndef EE_UI_HTMLTEXTINPUT_HPP +#define EE_UI_HTMLTEXTINPUT_HPP + +#include + +namespace EE { namespace UI { + +class EE_API HTMLTextInput : public UITextInput { + public: + static HTMLTextInput* New(); + + HTMLTextInput(); + + virtual Uint32 getType() const; + + virtual bool isType( const Uint32& type ) const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + + virtual std::vector getPropertiesImplemented() const; + + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + + virtual void onAutoSize(); + + Uint32 getHtmlSize() const; + + void setHtmlSize( Uint32 size ); + + protected: + HTMLTextInput( const std::string& tag ); + + Uint32 mHtmlSize{ 20 }; + bool mPacking{ false }; +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/models/modelselection.hpp b/include/eepp/ui/models/modelselection.hpp index 2ae6f38ef..51a7ad947 100644 --- a/include/eepp/ui/models/modelselection.hpp +++ b/include/eepp/ui/models/modelselection.hpp @@ -73,6 +73,13 @@ class EE_API ModelSelection { return *mIndexes.begin(); } + ModelIndex last() const { + Lock l( mMutex ); + if ( mIndexes.empty() ) + return {}; + return mIndexes.back(); + } + void removeAllMatching( std::function filter ); template void changeFromModel( Function f ) { diff --git a/include/eepp/ui/tools/htmlformatter.hpp b/include/eepp/ui/tools/htmlformatter.hpp index 38e00ae93..528aa6e08 100644 --- a/include/eepp/ui/tools/htmlformatter.hpp +++ b/include/eepp/ui/tools/htmlformatter.hpp @@ -21,6 +21,8 @@ class EE_API HTMLFormatter { static pugi::xml_node getLogicalNext( const pugi::xml_node& node ); static String collapseXmlWhitespace( const String& text, const pugi::xml_node& node ); + + static std::string HTMLtoXML( const std::string& layoutString ); }; }}} // namespace EE::UI::Tools diff --git a/include/eepp/ui/tools/uicodeeditorsplitter.hpp b/include/eepp/ui/tools/uicodeeditorsplitter.hpp index d414e9055..2ce6c9d83 100644 --- a/include/eepp/ui/tools/uicodeeditorsplitter.hpp +++ b/include/eepp/ui/tools/uicodeeditorsplitter.hpp @@ -20,6 +20,8 @@ class EE_API UICodeEditorSplitter { static const std::map getLocalDefaultKeybindings(); + static Uint32 getDefaultSwitchToTabModifier(); + class EE_API Client { public: virtual ~Client() {}; @@ -391,6 +393,8 @@ class EE_API UICodeEditorSplitter { UITabWidget* getCurTabWidget() const; + UITab* getTabFromWidget( UIWidget* ) const; + void setCanCreateSplitFn( std::function fn ); diff --git a/include/eepp/ui/tools/uidiffview.hpp b/include/eepp/ui/tools/uidiffview.hpp index c6d6e6ba2..4231763a6 100644 --- a/include/eepp/ui/tools/uidiffview.hpp +++ b/include/eepp/ui/tools/uidiffview.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace EE { namespace UI { @@ -13,7 +14,7 @@ namespace Tools { class UIDiffEditorPlugin; -class EE_API UIDiffView : public UIWidget { +class EE_API UIDiffView : public UIWidget, public WidgetCommandExecuter { public: enum class ViewMode { Unified, SideBySide }; enum class SubLineDiffAlgorithm { LCS, SES }; diff --git a/include/eepp/ui/uiapplication.hpp b/include/eepp/ui/uiapplication.hpp index 66a77eaa4..b9fcdbcc9 100644 --- a/include/eepp/ui/uiapplication.hpp +++ b/include/eepp/ui/uiapplication.hpp @@ -66,11 +66,14 @@ class EE_API UIApplication { //! Set if the application must show the memory manager result after closing the main window. void setShowMemoryManagerResult( bool show ); + bool showMemoryManagerResult() const; + String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; } protected: UISceneNode* mUISceneNode{ nullptr }; EE::Window::Window* mWindow{ nullptr }; + String::HashType mStyleSheetMarker{ 0 }; bool mDidRun{ false }; bool mShowMemoryManagerResult{ false }; }; diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index 36bc06195..edea80312 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -423,6 +423,10 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void setEnableColorPickerOnSelection( const bool& enableColorPickerOnSelection ); + const bool& getEnableInlineColorBoxes() const; + + void setEnableInlineColorBoxes( const bool& enableInlineColorBoxes ); + void setSyntaxDefinition( const SyntaxDefinition& definition ); void resetSyntaxDefinition(); @@ -862,6 +866,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { bool mHighlightMatchingBracket{ true }; bool mHighlightSelectionMatch{ true }; bool mEnableColorPickerOnSelection{ false }; + bool mEnableInlineColorBoxes{ false }; bool mVerticalScrollBarEnabled{ true }; bool mHorizontalScrollBarEnabled{ true }; bool mLongestLineWidthDirty{ true }; @@ -949,6 +954,15 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { MinimapConfig mMinimapConfig; Int64 mMinimapScrollOffset{ 0 }; std::unordered_map> mLinesWidthCache; + + struct ColorBoxData { + Int64 startColumn; + Int64 endColumn; + Color color; + }; + std::unordered_map>> + mColorBoxesCache; + Tools::UIDocFindReplace* mFindReplace{ nullptr }; struct PluginRequestedSpace { UICodeEditorPlugin* plugin; diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 0dc2470c7..1f1e8db2e 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -8,7 +8,7 @@ using namespace EE::Graphics; namespace EE { namespace UI { -enum UIFlag : Uint32 { +enum UIFlag : Int32 { UI_HALIGN_LEFT = TEXT_ALIGN_LEFT, UI_VALIGN_TOP = TEXT_ALIGN_TOP, UI_HALIGN_MASK = TEXT_HALIGN_MASK, @@ -44,6 +44,7 @@ enum UIFlag : Uint32 { UI_HIGHLIGHT = ( 1 << 28 ), UI_PARENT_ATTRIBUTE_CHANGED = ( 1 << 29 ), UI_LOADS_ITS_CHILDREN = ( 1 << 30 ), + UI_HTML_ELEMENT = ( 1 << 31 ), }; enum UINodeType { @@ -116,10 +117,16 @@ enum UINodeType { UI_TYPE_HTML_TABLE_HEAD, UI_TYPE_HTML_TABLE_BODY, UI_TYPE_HTML_TABLE_FOOTER, + UI_TYPE_HTML_INPUT, + UI_TYPE_HTML_TEXTINPUT, + UI_TYPE_HTML_TEXTAREA, UI_TYPE_HTML_TABLE_ROW, UI_TYPE_HTML_TABLE_CELL, UI_TYPE_DROPDOWNMODELLIST, UI_TYPE_DIFF_VIEW, + UI_TYPE_BR, + UI_TYPE_HTML_HTML, + UI_TYPE_HTML_BODY, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, UI_TYPE_USER = 200000, diff --git a/include/eepp/ui/uihtmltable.hpp b/include/eepp/ui/uihtmltable.hpp index 8df434851..1e4394120 100644 --- a/include/eepp/ui/uihtmltable.hpp +++ b/include/eepp/ui/uihtmltable.hpp @@ -9,6 +9,11 @@ namespace EE { namespace UI { class UIHTMLTableRow; class UIHTMLTableCell; +class UIHTMLTableHead; +class UIHTMLTableBody; +class UIHTMLTableFooter; + +enum class TableLayout { Auto, Fixed }; class EE_API UIHTMLTable : public UILayout { public: @@ -16,23 +21,46 @@ class EE_API UIHTMLTable : public UILayout { UIHTMLTable(); + void setTableLayout( TableLayout layout ); + + TableLayout getTableLayout() const; + virtual Uint32 getType() const; virtual bool isType( const Uint32& type ) const; virtual void updateLayout(); + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + protected: virtual Uint32 onMessage( const NodeMessage* Msg ); + void computeIntrinsicWidths() const; + SmallVector mRows; SmallVector mColWidths; SmallVector mCells; SmallVector mRowCellOffsets; + mutable SmallVector mColMinWidths; + mutable SmallVector mColMaxWidths; + mutable SmallVector mColSpecifiedWidths; + TableLayout mTableLayout{ TableLayout::Auto }; + mutable UIHTMLTableHead* mHead{ nullptr }; + mutable UIHTMLTableBody* mBody{ nullptr }; + mutable UIHTMLTableFooter* mFooter{ nullptr }; + Float mCellpadding{ 0 }; + Float mCellspacing{ 0 }; }; class EE_API UIHTMLTableCell : public UIRichText { public: + friend class UIHTMLTable; + static UIHTMLTableCell* New( const std::string& tag ); explicit UIHTMLTableCell( const std::string& tag ); @@ -45,6 +73,8 @@ class EE_API UIHTMLTableCell : public UIRichText { Uint32 getColspan() const; + virtual void onSizeChange(); + protected: Uint32 mColspan{ 1 }; }; diff --git a/include/eepp/ui/uiimage.hpp b/include/eepp/ui/uiimage.hpp index 666424cbd..69a476343 100644 --- a/include/eepp/ui/uiimage.hpp +++ b/include/eepp/ui/uiimage.hpp @@ -33,6 +33,10 @@ class EE_API UIImage : public UIWidget { virtual bool applyProperty( const StyleSheetProperty& attribute ); + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, const Uint32& propertyIndex = 0 ) const; diff --git a/include/eepp/ui/uilayout.hpp b/include/eepp/ui/uilayout.hpp index 9eed3cfed..698b6580a 100644 --- a/include/eepp/ui/uilayout.hpp +++ b/include/eepp/ui/uilayout.hpp @@ -21,6 +21,8 @@ class EE_API UILayout : public UIWidget { bool isPacking() const { return mPacking; } + bool isLayoutDirty() const { return mDirtyLayout; } + protected: friend class UISceneNode; @@ -45,6 +47,8 @@ class EE_API UILayout : public UIWidget { virtual void updateLayoutTree(); + virtual void updateLayoutWrappingContents(); + void setLayoutDirty(); bool setMatchParentIfNeededVerticalGrowth(); diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp index 031a86328..dca496292 100644 --- a/include/eepp/ui/uinode.hpp +++ b/include/eepp/ui/uinode.hpp @@ -28,6 +28,7 @@ class UISceneNode; class UITheme; class UINodeDrawable; class UIBorderDrawable; +class UIWidget; class EE_API UINode : public Node { public: @@ -1097,7 +1098,8 @@ class EE_API UINode : public Node { * @param defaultValue The default value if the property is not set (default: 0). * @return The computed length in pixels. */ - Float lengthFromValue( const CSS::StyleSheetProperty& property, const Float& defaultValue = 0 ); + Float lengthFromValue( const CSS::StyleSheetProperty& property, + const Float& defaultValue = 0 ) const; /** * @brief Evaluates a CSS length string to a dp value. @@ -1857,6 +1859,8 @@ class EE_API UINode : public Node { * @return The droppable hover color. */ Color getDroppableHoveringColor(); + + Float getAbsoluteFontSize( const UIWidget* widget ) const; }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiplacementutils.hpp b/include/eepp/ui/uiplacementutils.hpp new file mode 100644 index 000000000..139e6d4f0 --- /dev/null +++ b/include/eepp/ui/uiplacementutils.hpp @@ -0,0 +1,48 @@ +#include +#include +#include + +using namespace EE::Math; + +namespace EE::UI { + +enum class PlacementDirection { Right, Left, Bottom, Top, None }; + +enum class PlacementLayout { + Horizontal, // Strongly favors Right/Left (Ideal for tooltips/documentation) + Vertical // Strongly favors Bottom/Top (Ideal for dropdowns/menus) +}; + +struct PopupPlacementConfig { + Rectf areaRect; // The full visible screen/window area bounds + Rectf targetRect; // The main box we are attaching the popup to + Rectf alignRect; // Box to horizontally align with + Rectf avoidRect; // Box to strictly avoid overlapping (e.g., cursor line) + + PlacementLayout layoutBias = PlacementLayout::Horizontal; + bool supportHorizontal = true; // Set to false for Dropdowns + bool supportVertical = true; // Set to false if you strictly want side-panels + + Float userMaxWidth; + Float margin = 4.f; + + // Thresholds + Float minHorizontalSpace = 200.f; // Min width needed to trigger Horizontal bonus + Float minVerticalSpace = 100.f; // Min height needed to trigger Vertical bonus + Float minScoreHeight; // Minimum height considered "good" + Float maxScoreHeight; // Cap for height in the score calculation +}; + +struct PopupPlacementResult { + Rectf rect; + PlacementDirection direction = PlacementDirection::None; +}; + +class EE_API UIPlacementUtils { + public: + static PopupPlacementResult + findBestPopupPlacement( const PopupPlacementConfig& config, + const std::function& measureContentCb ); +}; + +} // namespace EE::UI diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 543a19b33..09a792071 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -26,7 +26,13 @@ class EE_API UIRichText : public UILayout { static UIRichText* NewH6() { return UIRichText::NewWithTag( "h6" ); }; - static UIRichText* NewBr() { return UIRichText::NewWithTag( "br" ); }; + static UIRichText* NewBr(); + + static UIRichText* NewHr(); + + static UIRichText* NewHtml(); + + static UIRichText* NewBody(); static UIRichText* NewDiv() { return UIRichText::NewWithTag( "div" ); }; @@ -36,8 +42,6 @@ class EE_API UIRichText : public UILayout { static UIRichText* NewBlockquote() { return UIRichText::NewWithTag( "blockquote" ); }; - explicit UIRichText( const std::string& tag = "richtext" ); - virtual Uint32 getType() const; virtual bool isType( const Uint32& type ) const; @@ -48,6 +52,10 @@ class EE_API UIRichText : public UILayout { virtual bool applyProperty( const StyleSheetProperty& attribute ); + virtual Float getMinIntrinsicWidth() const; + + virtual Float getMaxIntrinsicWidth() const; + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, const Uint32& propertyIndex = 0 ) const; @@ -126,6 +134,8 @@ class EE_API UIRichText : public UILayout { bool mSelecting{ false }; size_t mResizedCount{ 0 }; + explicit UIRichText( const std::string& tag = "richtext" ); + virtual Uint32 onMessage( const NodeMessage* Msg ); virtual Uint32 onMouseDown( const Vector2i& position, const Uint32& flags ); virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); @@ -145,11 +155,35 @@ class EE_API UIRichText : public UILayout { Int64 selCurInit() const { return mSelCurInit; } Int64 selCurEnd() const { return mSelCurEnd; } - void rebuildRichText(); + enum class IntrinsicMode { None, Min, Max }; + void rebuildRichText( RichText& richText, IntrinsicMode mode = IntrinsicMode::None ); void positionChildren(); void updateDefaultSpansStyle(); }; +class EE_API UIHTMLHtml : public UIRichText { + public: + static UIHTMLHtml* New( const std::string& tag ); + virtual Uint32 getType() const override; + bool isType( const Uint32& type ) const override; + + protected: + UIHTMLHtml( const std::string& tag = "html" ); +}; + +class EE_API UIHTMLBody : public UIRichText { + public: + static UIHTMLBody* New( const std::string& tag ); + virtual Uint32 getType() const override; + bool isType( const Uint32& type ) const override; + bool applyProperty( const StyleSheetProperty& attribute ) override; + + protected: + bool mPropagatedBackground{ false }; + + UIHTMLBody( const std::string& tag = "body" ); +}; + }} // namespace EE::UI #endif diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 90d317498..5dc830291 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -304,8 +304,11 @@ class EE_API UISceneNode : public SceneNode { * * @param styleSheet The CSS StyleSheet to combine. * @param forceReloadStyle If true, forces immediate style reload (default: true). + * @param baseURI If the resource was loaded from an URI, pass the URI in order to solve + * relative paths in CSS */ - void combineStyleSheet( const CSS::StyleSheet& styleSheet, bool forceReloadStyle = true ); + void combineStyleSheet( const CSS::StyleSheet& styleSheet, bool forceReloadStyle = true, + URI baseURI = {} ); /** * @brief Combines an inline stylesheet with the existing one. @@ -315,9 +318,11 @@ class EE_API UISceneNode : public SceneNode { * @param inlineStyleSheet The CSS stylesheet as a string. * @param forceReloadStyle If true, forces immediate style reload (default: true). * @param marker Marker to associate with the new styles. + * @param baseURI If the resource was loaded from an URI, pass the URI in order to solve + * relative paths in CSS */ void combineStyleSheet( const std::string& inlineStyleSheet, bool forceReloadStyle = true, - const Uint32& marker = 0 ); + const Uint32& marker = 0, URI baseURI = {} ); /** * @brief Gets the reference to the current stylesheet. @@ -689,9 +694,30 @@ class EE_API UISceneNode : public SceneNode { /** Sets the document / scene URI used to resolve paths of inner elements */ void setURI( const URI& uri ); + /** Sets the document / scene URI used to resolve paths from a complete URI (with + * path+query+fragment+etc) */ + void setURIFromURL( const URI& url ); + /** @return the document / scene URI used to resolve paths of inner elements */ const URI& getURI() const { return mURI; } + /** Handles opening an specific URI */ + void openURL( URI uri ); + + /* Sets a callback to intercept the openURL calls, returns true if intercepted, false to leave + * the default openURL implementation handle it. + */ + void setURLInterceptorCb( std::function cb ) { mURLInterceptorCb = cb; }; + + /** + * Solves a relative path with no scheme or authority into a complete URI. + * @param baseURI If must solve from a specific baseURI it must be passed here. + */ + URI solveRelativePath( URI uri, URI baseURI = {} ); + + /** @return The document referer */ + URI getReferer() const { return mReferer; }; + protected: friend class EE::UI::UIWindow; friend class EE::UI::UIWidget; @@ -720,6 +746,8 @@ class EE_API UISceneNode : public SceneNode { Uint32 mCurOnSizeChangeListener{ 0 }; std::shared_ptr mThreadPool; URI mURI; + URI mReferer; + std::function mURLInterceptorCb; /** * @brief Protected constructor. @@ -856,8 +884,10 @@ class EE_API UISceneNode : public SceneNode { * the stylesheet's at-rules and loads them. * * @param styleSheet The stylesheet to process. + * @param baseURI If the resource was loaded from an URI, pass the URI in order to solve + * relative paths in CSS */ - void processStyleSheetAtRules( const CSS::StyleSheet& styleSheet ); + void processStyleSheetAtRules( const CSS::StyleSheet& styleSheet, URI baseURI = {} ); /** * @brief Loads font faces from @font-face rules. @@ -866,8 +896,10 @@ class EE_API UISceneNode : public SceneNode { * (files, URLs, VFS). * * @param styles Vector of stylesheet styles from @font-face rules. + * @param baseURI If the resource was loaded from an URI, pass the URI in order to solve + * relative paths in CSS */ - void loadFontFaces( const CSS::StyleSheetStyleVector& styles ); + void loadFontFaces( const CSS::StyleSheetStyleVector& styles, URI baseURI = {} ); /** * @brief Loads CSS files from URI @@ -926,6 +958,10 @@ class EE_API UISceneNode : public SceneNode { * @param to The root node of the subtree to theme. */ void setTheme( UITheme* theme, Node* to ); + + /** @return The document / scene URI used to resolve paths from a complete URI (with + * path+query+fragment+etc) */ + URI getURIFromURL( const URI& url ) const; }; }} // namespace EE::UI diff --git a/include/eepp/ui/uistate.hpp b/include/eepp/ui/uistate.hpp index 9ab668b2e..4cf6c7028 100644 --- a/include/eepp/ui/uistate.hpp +++ b/include/eepp/ui/uistate.hpp @@ -20,6 +20,8 @@ class EE_API UIState { StateDisabled, StateChecked, StateFocusWithin, + StateLink, + StateVisited, StateCount }; @@ -34,6 +36,8 @@ class EE_API UIState { StateFlagDisabled = 1 << StateDisabled, StateFlagChecked = 1 << StateChecked, StateFlagFocusWithin = 1 << StateFocusWithin, + StateFlagLink = 1 << StateLink, + StateFlagVisited = 1 << StateVisited, StateFlagCount = StateCount }; diff --git a/include/eepp/ui/uitextinput.hpp b/include/eepp/ui/uitextinput.hpp index d873824dc..4fa7c5e69 100644 --- a/include/eepp/ui/uitextinput.hpp +++ b/include/eepp/ui/uitextinput.hpp @@ -15,8 +15,12 @@ class UIMenuItem; class EE_API UITextInput : public UITextView, public TextDocument::Client { public: + enum class TextInputMode { Normal, Password }; + static UITextInput* New(); + static UITextInput* NewPassword(); + static UITextInput* NewWithTag( const std::string& tag ); virtual ~UITextInput(); @@ -119,6 +123,14 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { void setSelectAllDocOnTabNavigate( bool selectAllDocOnTabNavigate ); + UITextInput* setMode( TextInputMode mode ); + + TextInputMode getMode() const; + + const String& getBulletCharacter() const; + + void setBulletCharacter( const String& bulletCharacter ); + Client::Type getTextDocumentClientType() { return TextDocument::Client::Core; } protected: @@ -145,6 +157,9 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { Uint64 mLastExecuteEventId{ 0 }; String::HashType mLastCmdHash{ 0 }; HintDisplay mHintDisplay{ HintDisplay::Always }; + TextInputMode mMode{ TextInputMode::Normal }; + Text* mPassCache{ nullptr }; + String mBulletCharacter{ "●" }; UITextInput(); @@ -180,6 +195,8 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { virtual void onFontChanged(); + virtual void onFontStyleChanged(); + void onThemeLoaded(); virtual void onCursorPosChange(); @@ -190,6 +207,12 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { virtual void updateText(); + virtual void updatePass(); + + virtual void updateFontStyleConfig(); + + virtual Text& getVisibleTextCache(); + virtual void selCurInit( const Int32& init ); virtual void selCurEnd( const Int32& end ); diff --git a/include/eepp/ui/uitextinputpassword.hpp b/include/eepp/ui/uitextinputpassword.hpp deleted file mode 100644 index e4fef8936..000000000 --- a/include/eepp/ui/uitextinputpassword.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef EE_UICUITEXTINPUTPASSWORD_HPP -#define EE_UICUITEXTINPUTPASSWORD_HPP - -#include - -namespace EE { namespace UI { - -class EE_API UITextInputPassword : public UITextInput { - public: - static UITextInputPassword* New(); - - virtual ~UITextInputPassword(); - - virtual void draw(); - - virtual const String& getText() const; - - virtual UITextView* setText( const String& text ); - - const Text& getPassCache() const; - - const String& getBulletCharacter() const; - - void setBulletCharacter( const String& bulletCharacter ); - - protected: - UITextInputPassword(); - - Text mPassCache; - Vector2f mHintAlignOffset; - String mBulletCharacter; - - void updateText(); - - void updatePass( const String& pass ); - - void updateFontStyleConfig(); - - virtual void onStateChange(); - - virtual void onFontChanged(); - - virtual void onFontStyleChanged(); - - virtual Text& getVisibleTextCache(); -}; - -}} // namespace EE::UI - -#endif diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index 7ae634d11..2827ae3aa 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -26,6 +26,8 @@ class EE_API UITextSpan : public UIWidget { static UITextSpan* NewStrikethrough() { return NewWithTag( "s" ); } + static UITextSpan* NewFont() { return NewWithTag( "font" ); } + static UITextSpan* NewMark() { return NewWithTag( "mark" ); } static UITextSpan* NewCode() { return NewWithTag( "code" ); } diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index 423273ced..47490ad28 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -138,7 +138,7 @@ class EE_API UITextView : public UIWidget { Int32 mSelCurInit; Int32 mSelCurEnd; Uint32 mTextDrawHints{ 0 }; - std::vector mSelRectsCache; + SmallVector mSelRectsCache; Int32 mLastSelCurInit; Int32 mLastSelCurEnd; bool mSelecting; diff --git a/include/eepp/ui/uitheme.hpp b/include/eepp/ui/uitheme.hpp index 88061fce2..ecd7f9574 100644 --- a/include/eepp/ui/uitheme.hpp +++ b/include/eepp/ui/uitheme.hpp @@ -77,6 +77,8 @@ class EE_API UITheme : protected ResourceManagerMulti { const CSS::StyleSheet& getStyleSheet() const; + void setStyleSheet( CSS::StyleSheet&& styleSheet ); + void setStyleSheet( const CSS::StyleSheet& styleSheet ); const Float& getDefaultFontSize() const; diff --git a/include/eepp/ui/uitreeview.hpp b/include/eepp/ui/uitreeview.hpp index 2e0f57352..ea0785ff2 100644 --- a/include/eepp/ui/uitreeview.hpp +++ b/include/eepp/ui/uitreeview.hpp @@ -149,6 +149,9 @@ class EE_API UITreeView : public UIAbstractTableView { virtual size_t getItemCount() const; + std::vector getSelectionRange( const ModelIndex& start, + const ModelIndex& end ) const; + UITreeView::MetadataForIndex& getIndexMetadata( const ModelIndex& index ) const; virtual void onColumnSizeChange( const size_t& colIndex, bool fromUserInteraction = false ); diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index da167ff2c..1d68c78b7 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -303,6 +303,24 @@ class EE_API UIWidget : public UINode { */ UIWidget* setLayoutMarginBottom( const Float& marginBottom ); + UIWidget* setLayoutMarginLeftAuto( bool isAuto ); + + UIWidget* setLayoutMarginRightAuto( bool isAuto ); + + UIWidget* setLayoutMarginTopAuto( bool isAuto ); + + UIWidget* setLayoutMarginBottomAuto( bool isAuto ); + + UIWidget* setLayoutMarginAuto( bool left, bool right, bool top, bool bottom ); + + bool hasLayoutMarginLeftAuto() const; + + bool hasLayoutMarginRightAuto() const; + + bool hasLayoutMarginTopAuto() const; + + bool hasLayoutMarginBottomAuto() const; + /** * @brief Sets the layout margin for all sides in pixels. * @@ -486,6 +504,35 @@ class EE_API UIWidget : public UINode { */ PositionPolicy getLayoutPositionPolicy() const; + /** + * @brief Gets the minimum intrinsic width of the widget. + * + * The minimum intrinsic width is the absolute minimum width the widget needs + * to display its content without overflowing. For text, this is typically + * the width of the longest unbreakable word. + * + * @return The minimum intrinsic width in pixels. + */ + virtual Float getMinIntrinsicWidth() const; + + /** + * @brief Gets the maximum intrinsic width of the widget. + * + * The maximum intrinsic width is the ideal width of the widget if it had + * infinite horizontal space (i.e., no wrapping). + * + * @return The maximum intrinsic width in pixels. + */ + virtual Float getMaxIntrinsicWidth() const; + + /** + * @brief Invalidates the cached intrinsic width. + * + * Forces a recalculation of the intrinsic widths on the next call to + * getMinIntrinsicWidth() or getMaxIntrinsicWidth(). + */ + void invalidateIntrinsicSize(); + /** * @brief Loads widget configuration from an XML node. * @@ -497,8 +544,8 @@ class EE_API UIWidget : public UINode { virtual void loadFromXmlNode( const pugi::xml_node& node ); /** - * @brief Boolean that indicates if the widget is in charge of loading its children nodes - */ + * @brief Boolean that indicates if the widget is in charge of loading its children nodes + */ bool loadsItsChildren() const; /** @@ -1268,6 +1315,10 @@ class EE_API UIWidget : public UINode { */ virtual void onWidgetCreated(); + Float getPropertyWidth() const; + + Float getPropertyHeight() const; + protected: friend class UIManager; friend class UISceneNode; @@ -1294,6 +1345,17 @@ class EE_API UIWidget : public UINode { std::string mSkinName; std::vector mClasses; String mTooltipText; + mutable Float mMinIntrinsicWidth{ 0 }; + mutable Float mMaxIntrinsicWidth{ 0 }; + mutable bool mIntrinsicWidthsDirty{ true }; + Uint8 mMarginAuto{ 0 }; + + static constexpr Uint8 MarginAutoLeft = ( 1 << 0 ); + static constexpr Uint8 MarginAutoRight = ( 1 << 1 ); + static constexpr Uint8 MarginAutoTop = ( 1 << 2 ); + static constexpr Uint8 MarginAutoBottom = ( 1 << 3 ); + + void calculateAutoMargin(); /** * @brief Default constructor. @@ -1645,6 +1707,7 @@ class EE_API UIWidget : public UINode { /* @return The size of the widget when size policy is match_parent */ Sizef getSizeFromLayoutPolicy(); + UIWidget* setLayoutMarginAuto( Uint32 dir, bool isAuto ); }; }} // namespace EE::UI diff --git a/include/eepp/version.hpp b/include/eepp/version.hpp index 9f75a4156..6fe91c64e 100644 --- a/include/eepp/version.hpp +++ b/include/eepp/version.hpp @@ -39,7 +39,7 @@ class EE_API Version { static Uint32 getVersionNum(); /** @return The library version name: "eepp version major.minor.patch" */ - static std::string getVersionName(); + static std::string getVersionName( bool fullName = true ); /** @return The version codename */ static std::string getCodename(); diff --git a/premake4.lua b/premake4.lua index b847e4389..d30a9af3d 100644 --- a/premake4.lua +++ b/premake4.lua @@ -763,6 +763,8 @@ function add_static_links() "libwebp-static", "libpng-static", "md4c-static", + "gumbo-parser-static", + "brotli-static", } if not _OPTIONS["without-mojoal"] then @@ -1005,6 +1007,7 @@ function build_eepp( build_name ) "src/thirdparty/libwebp/src", "src/thirdparty/SheenBidi/Headers", "src/thirdparty/SheenBidi/Headers/SheenBidi", + "src/thirdparty/brotli/include", } defines { "PCRE2_STATIC", "PCRE2_CODE_UNIT_WIDTH=8", "ONIG_STATIC" } @@ -1193,7 +1196,7 @@ solution "eepp" set_targetdir("libs/" .. os.get_real() .. "/thirdparty/") defines { "FT2_BUILD_LIBRARY" } files { "src/thirdparty/freetype2/src/**.c" } - includedirs { "src/thirdparty/freetype2/include", "src/thirdparty/libpng" } + includedirs { "src/thirdparty/freetype2/include", "src/thirdparty/libpng", "src/thirdparty/brotli/include" } build_base_configuration( "freetype" ) project "pcre2-8-static" @@ -1359,6 +1362,14 @@ solution "eepp" build_base_configuration( "mojoal" ) end + project "brotli-static" + kind "StaticLib" + language "C" + set_targetdir("libs/" .. os.get_real() .. "/thirdparty/") + includedirs { "src/thirdparty/brotli/include", "src/thirdparty/brotli/include/brotli" } + files { "src/thirdparty/brotli/**.c" } + build_base_configuration( "brotli" ) + project "md4c-static" kind "StaticLib" language "C" @@ -1375,6 +1386,16 @@ solution "eepp" includedirs { "src/thirdparty/libyaml/include" } build_base_configuration( "libyaml" ) + project "gumbo-parser-static" + kind "StaticLib" + language "C" + set_targetdir("libs/" .. os.get_real() .. "/thirdparty/") + files { "src/thirdparty/gumbo-parser/**.c" } + if is_vs() then + includedirs { "src/thirdparty/gumbo-parser/visualc/include/" } + end + build_base_configuration( "gumbo-parser" ) + project "efsw-static" kind "StaticLib" language "C++" @@ -1637,6 +1658,13 @@ solution "eepp" files { "src/examples/ui_markdownview/*.cpp" } build_link_configuration( "eepp-ui-markdownview", true ) + project "eepp-ui-html" + set_kind() + language "C++" + includedirs { "src/thirdparty" } + files { "src/examples/ui_html/*.cpp" } + build_link_configuration( "eepp-ui-html", true ) + project "eepp-richtext" set_kind() language "C++" @@ -1749,6 +1777,9 @@ solution "eepp" files { "src/tools/ecode/**.cpp" } includedirs { "src/thirdparty/efsw/include", "src/thirdparty", "src/modules/eterm/include/", "src/modules/languages-syntax-highlighting/src" } links { "efsw-static", "eterm-static", "languages-syntax-highlighting-static", "libyaml-static" } + if os.is("windows") then + links { "gumbo-parser-static" } + end if not os.is("windows") and not os.is("haiku") then links { "pthread" } end diff --git a/premake5.lua b/premake5.lua index 96a1b27ff..a54118fe7 100644 --- a/premake5.lua +++ b/premake5.lua @@ -605,6 +605,8 @@ function add_static_links() "libwebp-static", "libpng-static", "md4c-static", + "gumbo-parser-static", + "brotli-static", } if not _OPTIONS["without-mojoal"] then @@ -839,6 +841,7 @@ function build_eepp( build_name ) "src/thirdparty/libwebp/src", "src/thirdparty/SheenBidi/Headers", "src/thirdparty/SheenBidi/Headers/SheenBidi", + "src/thirdparty/brotli/include", } add_static_links() @@ -922,7 +925,7 @@ function target_dir_lib(path) targetdir("libs/" .. os.target() .. "/x86_64/" .. path .. "/") filter "architecture:ARM" targetdir("libs/" .. os.target() .. "/arm/" .. path .. "/") - filter "architecture:ARM64" + filter "architecture:ARM64 or AARCH64" targetdir("libs/" .. os.target() .. "/arm64/" .. path .. "/") filter "architecture:universal" targetdir("libs/" .. os.target() .. "/universal/" .. path .. "/") @@ -937,14 +940,14 @@ function postsymlinklib_arch(name) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/x86/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:x86" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/x86_64/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:x86_64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:ARM" ) - postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:ARM64" ) + postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:AARCH64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/", name, "options:arch=arm64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/universal/", _MAIN_SCRIPT_DIR .. "/bin/", name, "architecture:universal" ) if name == "eepp" then postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/x86/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:x86" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/x86_64/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:x86_64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:ARM" ) - postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:ARM64" ) + postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:AARCH64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/arm64/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "options:arch=arm64" ) postsymlinklib( _MAIN_SCRIPT_DIR .. "/libs/" .. os.target() .. "/universal/", _MAIN_SCRIPT_DIR .. "/bin/unit_tests/", name, "architecture:universal" ) end @@ -1096,7 +1099,7 @@ workspace "eepp" language "C" defines { "FT2_BUILD_LIBRARY" } files { "src/thirdparty/freetype2/src/**.c" } - incdirs { "src/thirdparty/freetype2/include", "src/thirdparty/libpng" } + incdirs { "src/thirdparty/freetype2/include", "src/thirdparty/libpng", "src/thirdparty/brotli/include" } build_base_configuration( "freetype" ) target_dir_thirdparty() @@ -1268,6 +1271,14 @@ workspace "eepp" filter { "options:windows-mingw-build", "options:arch=arm64" } incdirs { remote_sdl2_arm64_cross_tools_path .."/include/" } + project "brotli-static" + kind "StaticLib" + language "C" + incdirs { "src/thirdparty/brotli/include", "src/thirdparty/brotli/include/brotli" } + files { "src/thirdparty/brotli/**.c" } + build_base_configuration( "brotli" ) + target_dir_thirdparty() + project "md4c-static" kind "StaticLib" language "C" @@ -1284,6 +1295,15 @@ workspace "eepp" build_base_configuration( "libyaml" ) target_dir_thirdparty() + project "gumbo-parser-static" + kind "StaticLib" + language "C" + files { "src/thirdparty/gumbo-parser/**.c" } + build_base_configuration( "gumbo-parser" ) + target_dir_thirdparty() + filter "action:vs*" + incdirs { "src/thirdparty/gumbo-parser/visualc/include/" } + project "efsw-static" kind "StaticLib" language "C++" @@ -1520,6 +1540,13 @@ workspace "eepp" files { "src/examples/ui_richtext/*.cpp" } build_link_configuration( "eepp-ui-richtext", true ) + project "eepp-ui-html" + set_kind() + language "C++" + incdirs { "src/thirdparty" } + files { "src/examples/ui_html/*.cpp" } + build_link_configuration( "eepp-ui-html", true ) + project "eepp-ui-markdownview" set_kind() language "C++" @@ -1633,6 +1660,8 @@ workspace "eepp" incdirs { "src/thirdparty/efsw/include", "src/thirdparty", "src/modules/eterm/include/", "src/modules/languages-syntax-highlighting/src" } links { "efsw-static", "eterm-static", "languages-syntax-highlighting-static", "libyaml-static" } build_link_configuration( "ecode", false ) + filter { "system:windows" } + links { "gumbo-parser-static" } filter { "system:windows", "action:not vs*" } buildoptions{ "-Wa,-mbig-obj" } linkoptions { "-Wl,--export-all-symbols" } diff --git a/projects/android-project/app/jni/eepp.mk b/projects/android-project/app/jni/eepp.mk index 7d54fe8a7..30813f7e4 100644 --- a/projects/android-project/app/jni/eepp.mk +++ b/projects/android-project/app/jni/eepp.mk @@ -28,6 +28,7 @@ EEPP_C_INCLUDES := \ $(EEPP_THIRD_PARTY_PATH)/oniguruma \ $(EEPP_THIRD_PARTY_PATH)/SheenBidi/Headers \ $(EEPP_THIRD_PARTY_PATH)/efsw/include \ + $(EEPP_THIRD_PARTY_PATH)/brotli/include \ $(EEPP_BASE_PATH)/modules/eterm/include \ $(EEPP_BASE_PATH)/modules/eterm/src \ $(EEPP_BASE_PATH)/modules/maps/include \ @@ -112,7 +113,7 @@ LOCAL_C_INCLUDES := $(EEPP_C_INCLUDES) LOCAL_SRC_FILES := $(foreach F, $(CODE_SRCS), $(addprefix $(dir $(F)),$(notdir $(wildcard $(LOCAL_PATH)/$(F))))) -LOCAL_STATIC_LIBRARIES := freetype libpng libwebp md4c pcre2 oniguruma harfbuzz sheenbidi +LOCAL_STATIC_LIBRARIES := freetype libpng libwebp md4c pcre2 oniguruma harfbuzz sheenbidi gumbo-parser brotli LOCAL_SHARED_LIBRARIES := SDL2 @@ -153,7 +154,7 @@ LOCAL_MODULE := freetype APP_SUBDIRS := $(patsubst $(LOCAL_PATH)/%, %, $(shell find $(LOCAL_PATH)/src -type d)) -LOCAL_C_INCLUDES := $(foreach D, $(APP_SUBDIRS), $(LOCAL_PATH)/$(D)) $(LOCAL_PATH)/include $(EEPP_THIRD_PARTY_PATH)/libpng +LOCAL_C_INCLUDES := $(foreach D, $(APP_SUBDIRS), $(LOCAL_PATH)/$(D)) $(LOCAL_PATH)/include $(EEPP_THIRD_PARTY_PATH)/libpng $(EEPP_THIRD_PARTY_PATH)/brotli/include LOCAL_CFLAGS := -Os -DFT2_BUILD_LIBRARY LOCAL_SRC_FILES += $(foreach F, $(APP_SUBDIRS), $(addprefix $(F)/,$(notdir $(wildcard $(LOCAL_PATH)/$(F)/*.c)))) @@ -341,7 +342,7 @@ LOCAL_PATH := $(EEPP_THIRD_PARTY_PATH) LOCAL_MODULE := sheenbidi -SHEENBIDI_SRCS := SheenBidi/Source/**.c +SHEENBIDI_SRCS := SheenBidi/Source/**.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/SheenBidi/Headers LOCAL_CFLAGS := -Os -I$(LOCAL_PATH)/freetype2/include @@ -368,6 +369,40 @@ LOCAL_SRC_FILES := $(foreach F, $(LIBYAML_SRCS), $(addprefix $(dir $(F)),$(not include $(BUILD_STATIC_LIBRARY) #*************** LIBYAML *************** +#*************** GUMBOPARSER *************** +include $(CLEAR_VARS) + +LOCAL_PATH := $(EEPP_THIRD_PARTY_PATH) + +LOCAL_MODULE := gumbo-parser + +GUMBOPARSER_SRCS := gumbo-parser/*.c + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/gumbo-parser +LOCAL_CFLAGS := -Os + +LOCAL_SRC_FILES := $(foreach F, $(GUMBOPARSER_SRCS), $(addprefix $(dir $(F)),$(notdir $(wildcard $(LOCAL_PATH)/$(F))))) + +include $(BUILD_STATIC_LIBRARY) +#*************** GUMBOPARSER *************** + +#*************** BROTLI *************** +include $(CLEAR_VARS) + +LOCAL_PATH := $(EEPP_THIRD_PARTY_PATH) + +LOCAL_MODULE := brotli + +BROTLI_SRCS := brotli/common/*.c brotli/dec/*.c + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/brotli $(LOCAL_PATH)/brotli/include +LOCAL_CFLAGS := -Os + +LOCAL_SRC_FILES := $(foreach F, $(BROTLI_SRCS), $(addprefix $(dir $(F)),$(notdir $(wildcard $(LOCAL_PATH)/$(F))))) + +include $(BUILD_STATIC_LIBRARY) +#*************** BROTLI *************** + #*************** MD4C *************** include $(CLEAR_VARS) @@ -375,7 +410,7 @@ LOCAL_PATH := $(EEPP_THIRD_PARTY_PATH) LOCAL_MODULE := md4c -MD4C_SRCS := md4c/*.c +MD4C_SRCS := md4c/*.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/md4c LOCAL_CFLAGS := -Os diff --git a/projects/freebsd/ecode/build.app.sh b/projects/freebsd/ecode/build.app.sh index 69672a121..a310cedf4 100755 --- a/projects/freebsd/ecode/build.app.sh +++ b/projects/freebsd/ecode/build.app.sh @@ -32,14 +32,17 @@ while [ $# -gt 0 ]; do done CONFIG_NAME= +SO_LIB_PATH= if command -v premake5 &> /dev/null then premake5 gmake || exit CONFIG_NAME=release_x86_64 + SO_LIB_PATH=../../../libs/bsd/x86_64/libeepp.so elif command -v premake4 &> /dev/null then premake4 gmake || exit CONFIG_NAME=release + SO_LIB_PATH=../../../libs/bsd/libeepp.so else echo "Neither premake5 nor premake4 is available. Please install one." exit 1 @@ -63,7 +66,7 @@ chmod +x AppRun cp AppRun ecode.app/ cp ecode.desktop ecode.app/ cp ../../../bin/assets/icon/ecode.png ecode.app/ecode.png -cp ../../../libs/bsd/x86_64/libeepp.so ecode.app/libs/ +cp $SO_LIB_PATH ecode.app/libs/ cp ../../../bin/ecode ecode.app/ecode.bin cp -L /usr/local/lib/libSDL2-2.0.so.0 ecode.app/libs/ strip ecode.app/libs/libSDL2-2.0.so.0 diff --git a/projects/linux/ecode/build.app.sh b/projects/linux/ecode/build.app.sh index 9dfb66553..bffdb59a7 100755 --- a/projects/linux/ecode/build.app.sh +++ b/projects/linux/ecode/build.app.sh @@ -41,14 +41,17 @@ if [ "$ARCH" = "aarch64" ]; then fi CONFIG_NAME= +SO_LIB_PATH= if command -v premake4 &> /dev/null then premake4 $DEBUG_SYMBOLS $STATIC_CPP gmake || exit CONFIG_NAME=release + SO_LIB_PATH=../../../libs/linux/libeepp.so elif command -v premake5 &> /dev/null then premake5 $DEBUG_SYMBOLS $STATIC_CPP gmake || exit CONFIG_NAME=release_"$ARCH" + SO_LIB_PATH=../../../libs/linux/$ARCH/libeepp.so else echo "Neither premake5 nor premake4 is available. Please install one." exit 1 @@ -63,7 +66,7 @@ chmod +x AppRun cp AppRun ecode.app/ cp ecode.desktop ecode.app/ cp ../../../bin/assets/icon/ecode.png ecode.app/ecode.png -cp ../../../libs/linux/libeepp.so ecode.app/libs/ +cp "$SO_LIB_PATH" ecode.app/libs/ cp ../../../bin/ecode ecode.app/ecode.bin cp -L "$(bash ../scripts/find_most_recent_sdl2.sh --arch="$ARCH")" ecode.app/libs/ || exit ${STRIP:-strip} ecode.app/libs/libSDL2-2.0.so.0 diff --git a/projects/macos/ecode/build.app.sh b/projects/macos/ecode/build.app.sh index 7437c64ea..9b7985f06 100755 --- a/projects/macos/ecode/build.app.sh +++ b/projects/macos/ecode/build.app.sh @@ -23,14 +23,17 @@ done SDL2_CONFIG=$(which sdl2-config) CONFIG_NAME= ARCH_PATH= +DYLIB_PATH= if command -v premake4 &> /dev/null then CONFIG_NAME=release + DYLIB_PATH=../../../libs/macosx/libeepp.dylib elif command -v premake5 &> /dev/null then CONFIG_NAME=release_arm64 ARCH_PATH="arm64/" + DYLIB_PATH=../../../libs/macosx/"$ARCH_PATH"libeepp.dylib else echo "Neither premake5 nor premake4 is available. Please install one." exit 1 @@ -66,7 +69,7 @@ fi cat Info.plist.tpl | sed "s/ECODE_VERSION_STRING/${ECODE_VERSION}/g" | sed "s/ECODE_MAJOR_VERSION/${ECODE_MAJOR_VERSION}/g" | sed "s/ECODE_MINOR_VERSION/${ECODE_MINOR_VERSION}/g" > Info.plist cp Info.plist ecode.app/Contents/ rm Info.plist -cp ../../../libs/macosx/"$ARCH_PATH"libeepp.dylib ecode.app/Contents/MacOS +cp $DYLIB_PATH ecode.app/Contents/MacOS cp ../../../bin/ecode ecode.app/Contents/MacOS if [ -z "$SDL2_CONFIG" ]; then diff --git a/projects/mingw32/make.sh b/projects/mingw32/make.sh index 21f970a61..c32940fe4 100755 --- a/projects/mingw32/make.sh +++ b/projects/mingw32/make.sh @@ -31,14 +31,14 @@ elif [[ "$CONFIG" == *"arm64"* && "$(uname -m)" == "x86_64" ]]; then TAR_FILE_NAME="$FILE_NAME.tar.xz" RENAMED_FOLDER="llvm-mingw" BIN_PATH="$(pwd)/$RENAMED_FOLDER/bin" - + if [[ ! -f "$TAR_FILE_NAME" ]]; then echo "Downloading $TAR_FILE_NAME..." curl -LO "$URL" || { echo "Download failed!"; exit 1; } else echo "$TAR_FILE_NAME already exists. Skipping download." fi - + if [[ ! -d "$RENAMED_FOLDER" ]]; then echo "Extracting $TAR_FILE_NAME..." tar -xf "$TAR_FILE_NAME" || { echo "Extraction failed!"; exit 1; } @@ -46,7 +46,7 @@ elif [[ "$CONFIG" == *"arm64"* && "$(uname -m)" == "x86_64" ]]; then else echo "$RENAMED_FOLDER directory already exists. Skipping extraction." fi - + export PATH="$PATH:$BIN_PATH" echo "Added $BIN_PATH to PATH." fi diff --git a/flake.lock b/projects/nix/flake.lock similarity index 100% rename from flake.lock rename to projects/nix/flake.lock diff --git a/flake.nix b/projects/nix/flake.nix similarity index 100% rename from flake.nix rename to projects/nix/flake.nix diff --git a/projects/scripts/run_gdb_tests.sh b/projects/scripts/run_gdb_tests.sh new file mode 100755 index 000000000..0c0c5f2e1 --- /dev/null +++ b/projects/scripts/run_gdb_tests.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +cd ../../bin/unit_tests + +echo "=== Running eepp unit tests under GDB (xvfb) ===" + +xvfb-run -s "-screen 0 1280x1024x24" \ + gdb --batch --quiet --return-child-result \ + -ex "set confirm off" \ + -ex "set print thread-events off" \ + -ex "run" \ + -ex "bt full" \ + -ex "thread apply all bt full" \ + -ex "info registers" \ + -ex "quit" \ + --args ./eepp-unit_tests "$@" + +GDB_STATUS=$? + +if [ $GDB_STATUS -ne 0 ] && [ $GDB_STATUS -ne 139 ]; then + echo "❌ GDB exited with unexpected code: $GDB_STATUS" + exit 1 +fi + +echo "✅ Unit tests completed (GDB status: $GDB_STATUS)" diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index 144de5f92..31dc7b025 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -658,6 +658,27 @@ String::HashType String::hash( const String& str ) { str.size() * sizeof( String::StringBaseType ) ); } +String::HashType String::hashToLower( const std::string& str ) { + return String::hashToLower( str.c_str(), str.length() ); +} + +String::HashType String::hashToLower( const std::string_view& str ) { + return String::hashToLower( str.data(), str.length() ); +} + +String::HashType String::hashToLower( const String::View& str ) { + String::HashType hash = 5381; + for ( size_t i = 0; i < str.size(); ++i ) { + int c = str[i]; + hash = ( ( hash << 5 ) + hash ) + ( c >= 'A' && c <= 'Z' ? c + 32 : c ); + } + return hash; +} + +String::HashType String::hashToLower( const String& str ) { + return String::hashToLower( str.view() ); +} + bool String::isCharacter( const int& value ) { return ( value >= 32 && value <= 126 ) || ( value >= 161 && value <= 255 ) || ( value == 9 ); } @@ -1293,6 +1314,11 @@ bool String::startsWith( const String& haystack, const String& needle ) { std::equal( needle.begin(), needle.end(), haystack.begin() ); } +bool String::startsWith( String::View haystack, String::View needle ) { + return needle.length() <= haystack.length() && + std::equal( needle.begin(), needle.end(), haystack.begin() ); +} + bool String::startsWith( const char* haystack, const char* needle ) { return strncmp( needle, haystack, strlen( needle ) ) == 0; } @@ -1302,6 +1328,45 @@ bool String::startsWith( std::string_view haystack, std::string_view needle ) { std::equal( needle.begin(), needle.end(), haystack.begin() ); } +bool String::istartsWith( const std::string& haystack, const std::string& needle ) { + return needle.length() <= haystack.length() && + std::equal( needle.begin(), needle.end(), haystack.begin(), []( char c1, char c2 ) { + return std::tolower( c1 ) == std::tolower( c2 ); + } ); +} + +bool String::istartsWith( const String& haystack, const String& needle ) { + return needle.length() <= haystack.length() && + std::equal( needle.begin(), needle.end(), haystack.begin(), + []( String::StringBaseType c1, String::StringBaseType c2 ) { + return std::tolower( c1 ) == std::tolower( c2 ); + } ); +} + +bool String::istartsWith( String::View haystack, String::View needle ) { + return needle.length() <= haystack.length() && + std::equal( needle.begin(), needle.end(), haystack.begin(), + []( String::StringBaseType c1, String::StringBaseType c2 ) { + return std::tolower( c1 ) == std::tolower( c2 ); + } ); +} + +bool String::istartsWith( const char* haystack, const char* needle ) { + size_t needleLen = strlen( needle ); + if ( needleLen > strlen( haystack ) ) + return false; + return std::equal( needle, needle + needleLen, haystack, []( char c1, char c2 ) { + return std::tolower( c1 ) == std::tolower( c2 ); + } ); +} + +bool String::istartsWith( std::string_view haystack, std::string_view needle ) { + return needle.length() <= haystack.length() && + std::equal( needle.begin(), needle.end(), haystack.begin(), []( char c1, char c2 ) { + return std::tolower( c1 ) == std::tolower( c2 ); + } ); +} + bool String::endsWith( const std::string& haystack, const std::string& needle ) { return needle.length() <= haystack.length() && haystack.compare( haystack.size() - needle.size(), needle.size(), needle ) == 0; @@ -1312,6 +1377,11 @@ bool String::endsWith( const String& haystack, const String& needle ) { haystack.compare( haystack.size() - needle.size(), needle.size(), needle ) == 0; } +bool String::endsWith( String::View haystack, String::View needle ) { + return needle.length() <= haystack.length() && + haystack.compare( haystack.size() - needle.size(), needle.size(), needle ) == 0; +} + bool String::contains( const std::string& haystack, const std::string& needle ) { return haystack.find( needle ) != std::string::npos; } @@ -1345,7 +1415,7 @@ bool String::icontains( std::string_view haystack, std::string_view needle ) { } ) != haystack.end(); } -void String::replaceAll( std::string& target, const std::string& that, const std::string& with ) { +void String::replaceAll( std::string& target, std::string_view that, std::string_view with ) { std::string::size_type pos = 0; while ( ( pos = target.find( that, pos ) ) != std::string::npos ) { @@ -2432,14 +2502,12 @@ void String::readBySeparatorStoppable( std::string_view buf, } size_t String::countLines( std::string_view text ) { - const char* startPtr = text.data(); - const char* endPtr = text.data() + text.size(); - size_t count = 0; - if ( startPtr != endPtr ) { - count = 1 + *startPtr == '\n' ? 1 : 0; - while ( ++startPtr && startPtr != endPtr ) - count += ( '\n' == *startPtr ) ? 1 : 0; - } + if ( text.empty() ) + return 0; + size_t count = 1; + for ( const auto& c : text ) + if ( c == '\n' ) + count++; return count; } @@ -2468,14 +2536,12 @@ void String::readBySeparatorStoppable( String::View buf, } size_t String::countLines( String::View text ) { - const String::StringBaseType* startPtr = text.data(); - const String::StringBaseType* endPtr = text.data() + text.size(); - size_t count = 0; - if ( startPtr != endPtr ) { - count = 1 + *startPtr == '\n' ? 1 : 0; - while ( ++startPtr && startPtr != endPtr ) - count += ( '\n' == *startPtr ) ? 1 : 0; - } + if ( text.empty() ) + return 0; + size_t count = 1; + for ( const auto& c : text ) + if ( c == '\n' ) + count++; return count; } diff --git a/src/eepp/core/version.cpp b/src/eepp/core/version.cpp index 280665a32..7336fc325 100644 --- a/src/eepp/core/version.cpp +++ b/src/eepp/core/version.cpp @@ -14,9 +14,10 @@ Uint32 Version::getVersionNum() { return EEPP_VERSIONNUM( ver.major, ver.minor, ver.patch ); } -std::string Version::getVersionName() { +std::string Version::getVersionName( bool fullName ) { Version ver = getVersion(); - return String::format( "eepp version %d.%d.%d", ver.major, ver.minor, ver.patch ); + return fullName ? String::format( "eepp version %d.%d.%d", ver.major, ver.minor, ver.patch ) + : String::format( "%d.%d.%d", ver.major, ver.minor, ver.patch ); } std::string Version::getCodename() { diff --git a/src/eepp/graphics/drawablesearcher.cpp b/src/eepp/graphics/drawablesearcher.cpp index cbd79c987..78f80a80a 100644 --- a/src/eepp/graphics/drawablesearcher.cpp +++ b/src/eepp/graphics/drawablesearcher.cpp @@ -107,7 +107,8 @@ static Drawable* parseDataURI( const std::string& name ) { return drawable; } -Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSearchSprite ) { +Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSearchSprite, + Network::URI referer ) { Drawable* drawable = NULL; if ( name.size() ) { @@ -147,6 +148,13 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea } else if ( String::startsWith( name, "file://" ) ) { std::string filePath( name.substr( 7 ) ); +#if EE_PLATFORM == EE_PLATFORM_WIN + if ( filePath.size() >= 3 && filePath[0] == '/' && String::isLetter( filePath[1] ) && + filePath[2] == ':' ) { + filePath = filePath.substr( 1 ); + } +#endif + drawable = TextureFactory::instance()->getByName( filePath ); if ( NULL == drawable ) { @@ -164,17 +172,25 @@ Drawable* DrawableSearcher::searchByName( const std::string& name, bool firstSea 1, 1, 4, Color::Transparent, false, Texture::ClampMode::ClampToEdge, false, false, name ); + std::map headers; + if ( !referer.empty() ) + headers["referer"] = referer.toString(); + Http::getAsync( - [texture]( const Http&, Http::Request&, Http::Response& response ) { - if ( !response.getBody().empty() ) { + [texture, name]( const Http&, Http::Request&, Http::Response& response ) { + if ( response.isOK() && !response.getBody().empty() ) { Image image( (const Uint8*)&response.getBody()[0], response.getBody().size() ); if ( image.getPixels() != NULL ) texture->replace( &image ); + } else { + Log::debug( "DrawableSearcher::searchByName: could not download image: " + "%s. Error: %d\n%s", + name, response.getStatus(), response.getBody() ); } }, - URI( name ), Seconds( 5 ) ); + URI( name ), Seconds( 5 ), {}, headers ); } drawable = texture; diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 997b51a96..8bfc3705c 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -95,7 +95,7 @@ void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, cons span.size ); } }, - []( const Sizef& ) {} }, + []( const CustomBlock& ) {} }, span.block ); } } @@ -182,9 +182,9 @@ Vector2f RichText::findCharacterPos( Int64 index ) const { return { 0, 0 }; } -std::vector RichText::getSelectionRects() const { +SmallVector RichText::getSelectionRects() const { const_cast( this )->updateLayout(); - std::vector rects; + SmallVector rects; if ( mSelection.start == mSelection.end ) return rects; @@ -272,19 +272,19 @@ void RichText::addSpan( const String& text, const FontStyleConfig& style ) { span->setString( text ); span->setStyleConfig( style ); mBlocks.push_back( span ); // Implicitly constructs the variant's Text alternative - mNeedsLayoutUpdate = true; + invalidateLayout(); } void RichText::addDrawable( std::shared_ptr drawable ) { if ( !drawable ) return; mBlocks.push_back( drawable ); - mNeedsLayoutUpdate = true; + invalidateLayout(); } -void RichText::addCustomSize( const Sizef& size ) { - mBlocks.push_back( size ); - mNeedsLayoutUpdate = true; +void RichText::addCustomSize( const Sizef& size, bool isBlock ) { + mBlocks.push_back( CustomBlock{ size, isBlock } ); + invalidateLayout(); } void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Color color, @@ -307,30 +307,30 @@ void RichText::clear() { mBlocks.clear(); mLines.clear(); mSelection = { 0, 0 }; - mNeedsLayoutUpdate = true; + invalidateLayout(); } void RichText::setFontStyleConfig( const FontStyleConfig& styleConfig ) { mDefaultStyle = styleConfig; - mNeedsLayoutUpdate = true; + invalidateLayout(); } void RichText::setAlign( Uint32 align ) { if ( mAlign != align ) { mAlign = align; - mNeedsLayoutUpdate = true; + invalidateLayout(); } } void RichText::setMaxWidth( Float width ) { if ( mMaxWidth != width ) { mMaxWidth = width; - mNeedsLayoutUpdate = true; + invalidateLayout(); } } void RichText::invalidate() { - mNeedsLayoutUpdate = true; + invalidateLayout(); for ( auto& block : mBlocks ) { if ( auto pText = std::get_if>( &block ) ) { if ( *pText ) @@ -339,6 +339,78 @@ void RichText::invalidate() { } } +Float RichText::getMinIntrinsicWidth() { + Float minW = 0; + for ( auto& block : mBlocks ) { + if ( auto pText = std::get_if>( &block ) ) { + auto& span = *pText; + if ( !span || span->getString().empty() ) + continue; + const String& s = span->getString(); + size_t start = 0; + size_t end = 0; + while ( start < s.size() ) { + while ( start < s.size() && ( s[start] == ' ' || s[start] == '\t' || + s[start] == '\n' || s[start] == '\r' ) ) + start++; + end = start; + while ( end < s.size() && + !( s[end] == ' ' || s[end] == '\t' || s[end] == '\n' || s[end] == '\r' ) ) + end++; + if ( start < end ) { + minW = std::max( minW, Text::getTextWidth( s.substr( start, end - start ), + span->getFontStyleConfig() ) ); + } + start = end; + } + } else if ( auto pDrawable = std::get_if>( &block ) ) { + minW = std::max( minW, ( *pDrawable )->getPixelsSize().getWidth() ); + } else if ( auto pSize = std::get_if( &block ) ) { + minW = std::max( minW, pSize->size.getWidth() ); + } + } + return minW; +} + +Float RichText::getMaxIntrinsicWidth() { + Float maxW = 0; + Float curX = 0; + for ( auto& block : mBlocks ) { + if ( auto pText = std::get_if>( &block ) ) { + auto& span = *pText; + if ( !span || span->getString().empty() ) + continue; + + const String& s = span->getString(); + size_t start = 0; + size_t end = 0; + while ( ( end = s.find( '\n', start ) ) != String::InvalidPos ) { + curX += Text::getTextWidth( s.substr( start, end - start ), + span->getFontStyleConfig(), 4, span->getTextHints() ); + maxW = std::max( maxW, curX ); + curX = 0; + start = end + 1; + } + curX += Text::getTextWidth( s.substr( start ), span->getFontStyleConfig(), 4, + span->getTextHints() ); + } else if ( auto pDrawable = std::get_if>( &block ) ) { + curX += ( *pDrawable )->getPixelsSize().getWidth(); + } else if ( auto pSize = std::get_if( &block ) ) { + if ( pSize->isBlock ) { + if ( curX > 0 ) { + maxW = std::max( maxW, curX ); + curX = 0; + } + maxW = std::max( maxW, pSize->size.getWidth() ); + } else { + curX += pSize->size.getWidth(); + } + } + } + maxW = std::max( maxW, curX ); + return maxW; +} + void RichText::updateLayout() { if ( !mNeedsLayoutUpdate ) return; @@ -420,15 +492,24 @@ void RichText::updateLayout() { } } else { // Drawable or CustomSize Sizef blockSize; + bool isBlock = false; if ( auto pDrawable = std::get_if>( &block ) ) { auto& drawable = *pDrawable; blockSize = drawable ? drawable->getPixelsSize() : Sizef(); - } else if ( auto pSize = std::get_if( &block ) ) { - blockSize = *pSize; + } else if ( auto pSize = std::get_if( &block ) ) { + blockSize = pSize->size; + isBlock = pSize->isBlock; + } + + if ( isBlock && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; } // Wrap if needed - if ( mMaxWidth > 0 && curX + blockSize.getWidth() > mMaxWidth && curX > 0 ) { + if ( mMaxWidth > 0 && !isBlock && + ( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && curX > 0 ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); curX = 0; @@ -451,7 +532,7 @@ void RichText::updateLayout() { curX += blockSize.getWidth(); currentLine.width += blockSize.getWidth(); - if ( mMaxWidth > 0 && curX + blockSize.getWidth() > mMaxWidth && curX > 0 ) { + if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); curX = 0; @@ -511,4 +592,8 @@ Sizef RichText::getSize() { return mSize; } +void RichText::invalidateLayout() { + mNeedsLayoutUpdate = true; +} + }} // namespace EE::Graphics diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index f06be814d..712f54f99 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -81,7 +81,7 @@ Uint32 Text::stringToStyleFlag( const std::string& str ) { flags |= Text::Bold; else if ( "italic" == cur ) flags |= Text::Italic; - else if ( "strikethrough" == cur ) + else if ( "strikethrough" == cur || "line-through" == cur ) flags |= Text::StrikeThrough; else if ( "shadowed" == cur || "shadow" == cur ) flags |= Text::Shadow; @@ -577,13 +577,8 @@ void Text::create( Font* font, const String& text, Color FontColor, Color FontSh invalidate(); } -void Text::onNewString() { - mColorsNeedUpdate = true; - mGeometryNeedUpdate = true; - mCachedWidthNeedUpdate = true; +void Text::checkColorEmojis() { mContainsColorEmoji = false; - mVisualLinesNeedUpdate = true; - mTextHints = mString.getTextHints(); if ( mFontStyleConfig.Font && FontManager::instance()->getColorEmojiFont() != nullptr ) { if ( mFontStyleConfig.Font->getType() == FontType::TTF ) { FontTrueType* fontTrueType = static_cast( mFontStyleConfig.Font ); @@ -593,6 +588,15 @@ void Text::onNewString() { } } +void Text::onNewString() { + mColorsNeedUpdate = true; + mGeometryNeedUpdate = true; + mCachedWidthNeedUpdate = true; + mVisualLinesNeedUpdate = true; + mTextHints = mString.getTextHints(); + checkColorEmojis(); +} + bool Text::setString( const String::View& string ) { if ( mString.view() != string ) { mString = string; @@ -630,6 +634,7 @@ void Text::setFont( Font* font ) { mGeometryNeedUpdate = true; mCachedWidthNeedUpdate = true; mVisualLinesNeedUpdate = true; + checkColorEmojis(); } } @@ -783,8 +788,7 @@ Vector2f Text::findCharacterPos( std::size_t index ) const { std::size_t visualLinesSize = mVisualLines.size(); std::size_t lineIndex = const_cast( this )->findVisualLineFromCharIndex( index ); - Float vspace = static_cast( - mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) ); + Float vspace = getLineSpacing(); Float y = lineIndex * vspace; Float centerDiffX = 0; @@ -852,7 +856,7 @@ Int32 Text::findCharacterFromPos( const Vector2i& pos, bool returnNearest ) cons const_cast( this )->ensureVisualLinesUpdate(); - Float vspace = mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ); + Float vspace = getLineSpacing(); int lineIndex = std::floor( pos.y / vspace ); std::size_t visualLinesSize = mVisualLines.size(); @@ -1605,13 +1609,10 @@ Float Text::getTextWidth() { Float Text::getTextHeight() { cacheWidth(); - return NULL != mFontStyleConfig.Font - ? mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) * - ( mLinesWidth.empty() ? 1 : mLinesWidth.size() ) - : 0; + return getLineSpacing() * ( mLinesWidth.empty() ? 1 : mLinesWidth.size() ); } -Float Text::getLineSpacing() { +Float Text::getLineSpacing() const { return NULL != mFontStyleConfig.Font ? mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) : 0; @@ -1698,6 +1699,9 @@ void Text::draw( const Float& X, const Float& Y, const Vector2f& scale, const Fl return; } + if ( mColors.empty() ) + return; + Texture* texture = mFontStyleConfig.Font->getTexture( mFontStyleConfig.CharacterSize ); if ( !texture ) return; @@ -1796,7 +1800,7 @@ void Text::ensureGeometryUpdate() { if ( mCachedWidthNeedUpdate ) mLinesWidth.clear(); - mBounds = Rectf(); + mBounds = Rectf::Zero; // No font or text: nothing to draw if ( !mFontStyleConfig.Font || mString.empty() ) @@ -1833,8 +1837,7 @@ void Text::ensureGeometryUpdate() { Glyph hglyph = mFontStyleConfig.Font->getGlyph( L' ', mFontStyleConfig.CharacterSize, bold, reqItalic ); Float hspace = static_cast( hglyph.advance ); - Float vspace = static_cast( - mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) ); + Float vspace = getLineSpacing(); Float x = mInitialOffset.x; Float y = mFontStyleConfig.CharacterSize; @@ -2742,7 +2745,9 @@ Uint32 Text::getTotalVertices() { for ( const auto& ch : mString ) { lineHasChars = true; - if ( ' ' == ch || '\n' == ch || '\t' == ch || '\r' == ch ) { + if ( ' ' == ch ) + skipped++; + else if ( '\n' == ch || '\t' == ch || '\r' == ch ) { lineHasChars = false; skipped++; @@ -2883,8 +2888,8 @@ size_t Text::findVisualLineFromCharIndex( size_t charIndex ) { return 0; } -std::vector Text::getSelectionRects( TextSelectionRange range ) { - std::vector rects; +SmallVector Text::getSelectionRects( TextSelectionRange range ) { + SmallVector rects; if ( range.start == range.end || !mFontStyleConfig.Font ) return rects; @@ -2902,8 +2907,7 @@ std::vector Text::getSelectionRects( TextSelectionRange range ) { ->getGlyph( ' ', mFontStyleConfig.CharacterSize, mFontStyleConfig.Style & Text::Bold, mFontStyleConfig.Style & Text::Italic ) .advance; - Float vspace = static_cast( - mFontStyleConfig.Font->getLineSpacing( mFontStyleConfig.CharacterSize ) ); + Float vspace = getLineSpacing(); for ( size_t i = startLine; i <= endLine; ++i ) { Float top = i * vspace; diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index 41579a368..b267d00e9 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -236,6 +236,17 @@ static inline Uint64 textLayoutHash( const String::View& string, Font* font, std::hash()( initialXOffset ) ); } +static LRULayoutCache& getLayoutCache( bool invalidate = false ) { + static LRULayoutCache sLayoutCache; + if ( invalidate ) + sLayoutCache.clear(); + return sLayoutCache; +} + +void TextLayout::clearLayoutCache() { + getLayoutCache( true ); +} + TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, const Uint32& characterSize, const Uint32& style, const Uint32& tabWidth, const Float& outlineThickness, @@ -243,7 +254,6 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, TextDirection baseDirection, LineWrapMode wrapMode, Uint32 wrapWidth, bool keepIndentation, Float initialXOffset ) { - static LRULayoutCache sLayoutCache; if ( !font || string.empty() ) { auto layout = std::make_shared(); @@ -258,7 +268,7 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, tabOffset, baseDirection, wrapMode, wrapWidth, keepIndentation, initialXOffset ); - auto cacheHit = sLayoutCache.get( hash ); + auto cacheHit = getLayoutCache().get( hash ); if ( cacheHit.has_value() ) return *cacheHit; } @@ -513,7 +523,7 @@ TextLayout::Cache TextLayout::layout( const String::View& string, Font* font, characterSize, style, tabWidth, outlineThickness, hspace ); } - sLayoutCache.put( hash, resultPtr ); + getLayoutCache().put( hash, resultPtr ); return resultPtr; } diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index b6a9a24d5..ed8bce07c 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -27,6 +27,17 @@ namespace EE { namespace Network { #define PACKET_BUFFER_SIZE ( 16384 ) +static std::string sDefaultUserAgent = "Mozilla/5.0 (Linux; x86_64) eepp-network/1.0 " + "Chrome/146.0.0.0 AppleWebKit/537.36 Safari/537.36"; + +void Http::setDefaultUserAgent( const std::string& userAgent ) { + sDefaultUserAgent = userAgent; +} + +std::string Http::getDefaultUserAgent() { + return sDefaultUserAgent; +} + std::string Http::Request::statusToString( Http::Request::Status status ) { switch ( status ) { case Connected: @@ -199,9 +210,11 @@ const Http::Request::CancelCallback& Http::Request::getCancelCallback() const { return mCancelCallback; } -void Http::Request::cancel() { +void Http::Request::cancel( bool resetCancelCallback ) { mCancel = true; setProgressCallback( {} ); + if ( resetCancelCallback ) + setCancelCallback( {} ); } const bool& Http::Request::isCancelled() const { @@ -220,7 +233,7 @@ std::string Http::Request::prepareTunnel( const Http& http ) { setField( "Host", String::format( "%s:%d", http.getHostName().c_str(), http.getPort() ) ); setField( "Proxy-Connection", "Keep-Alive" ); - setField( "User-Agent", "eepp-network" ); + setField( "User-Agent", sDefaultUserAgent ); for ( FieldTable::const_iterator i = mFields.begin(); i != mFields.end(); ++i ) out << i->first << ": " << i->second << "\r\n"; @@ -346,6 +359,10 @@ const char* Http::Response::statusToString( const Http::Response::Status& status return "Moved Temporarily"; case NotModified: return "Not Modified"; + case TemporaryRedirect: + return "Temporary Redirect"; + case PermanentRedirect: + return "Permanent Redirect"; // 4xx: client error case BadRequest: @@ -394,6 +411,8 @@ Http::Response::Status Http::Response::intAsStatus( const int& value ) { case MultipleChoices: case MovedPermanently: case MovedTemporarily: + case TemporaryRedirect: + case PermanentRedirect: case NotModified: case BadRequest: case Unauthorized: @@ -452,6 +471,10 @@ Http::Response::Status Http::Response::getStatus() const { return mStatus; } +bool Http::Response::isOK() const { + return mStatus >= 200 && mStatus < 300; +} + const char* Http::Response::getStatusDescription() const { switch ( mStatus ) { // 2xx: success @@ -474,8 +497,10 @@ const char* Http::Response::getStatusDescription() const { case MultipleChoices: return "The requested page can be accessed from several locations"; case MovedPermanently: + case PermanentRedirect: return "The requested page has permanently moved to a new location"; case MovedTemporarily: + case TemporaryRedirect: return "The requested page has temporarily moved to a new location"; case NotModified: return "For conditional requests, means the requested page hasn't changed and doesn't " @@ -1005,12 +1030,15 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // Check if the content is compressed std::string encoding( received.getField( "content-encoding" ) ); - compressed = encoding == "gzip" || encoding == "deflate"; + compressed = encoding == "gzip" || encoding == "deflate" || + encoding == "br"; if ( compressed ) { Compression::Mode compressionMode = - "gzip" == encoding ? Compression::MODE_GZIP - : Compression::MODE_DEFLATE; + "gzip" == encoding + ? Compression::MODE_GZIP + : ( "br" == encoding ? Compression::MODE_BROTLI + : Compression::MODE_DEFLATE ); inflateStream = IOStreamInflate::New( writeTo, compressionMode ); @@ -1044,7 +1072,9 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // If a redirection is requested, and requests follows // redirections, send a new request to the redirection location. if ( ( received.getStatus() == Response::MovedPermanently || - received.getStatus() == Response::MovedTemporarily ) && + received.getStatus() == Response::MovedTemporarily || + received.getStatus() == Response::PermanentRedirect || + received.getStatus() == Response::TemporaryRedirect ) && request.getFollowRedirect() ) { // Only continue redirecting if less than 10 redirections @@ -1070,7 +1100,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // Same host, expects a path in the same domain if ( uri.getHost().empty() || - uri.getHost() == getHost() ) { + uri.getHost() == getHostName() ) { return downloadRequest( newRequest, writeTo, timeout ); } else { @@ -1236,8 +1266,8 @@ Http::AsyncRequest::~AsyncRequest() { eeSAFE_DELETE( mStream ); } -void Http::AsyncRequest::cancel() { - mRequest.cancel(); +void Http::AsyncRequest::cancel( bool resetCancelCallback ) { + mRequest.cancel( resetCancelCallback ); } void Http::AsyncRequest::run() { @@ -1278,7 +1308,7 @@ Http::Request Http::prepareFields( const Http::Request& request ) { Request toSend( request ); if ( !toSend.hasField( "User-Agent" ) ) - toSend.setField( "User-Agent", "eepp-network" ); + toSend.setField( "User-Agent", sDefaultUserAgent ); if ( !toSend.hasField( "Accept" ) ) toSend.setField( "Accept", "*/*" ); @@ -1314,7 +1344,7 @@ Http::Request Http::prepareFields( const Http::Request& request ) { } if ( request.isCompressedResponse() ) - toSend.setField( "Accept-Encoding", "gzip, deflate" ); + toSend.setField( "Accept-Encoding", "gzip, deflate, br" ); return toSend; } @@ -1331,11 +1361,11 @@ bool Http::isProxied() const { return !mProxy.empty(); } -bool Http::setCancelRequest( Uint64 reqId ) { +bool Http::setCancelRequest( Uint64 reqId, bool resetCancelCallback ) { Lock l( mCurRequestsMutex ); auto found = mCurRequests.find( reqId ); if ( found != mCurRequests.end() ) { - found->second->cancel(); + found->second->cancel( resetCancelCallback ); return true; } return false; diff --git a/src/eepp/network/uri.cpp b/src/eepp/network/uri.cpp index 910090abf..754631b76 100644 --- a/src/eepp/network/uri.cpp +++ b/src/eepp/network/uri.cpp @@ -277,7 +277,7 @@ std::string URI::getQuery() const { return query; } -void URI::getFragment( const std::string& fragment ) { +void URI::setFragment( const std::string& fragment ) { mFragment.clear(); decode( fragment, mFragment ); } diff --git a/src/eepp/scene/keyevent.cpp b/src/eepp/scene/keyevent.cpp index 6647eb9f5..c7e2f43b3 100644 --- a/src/eepp/scene/keyevent.cpp +++ b/src/eepp/scene/keyevent.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace EE { namespace Scene { @@ -40,6 +41,32 @@ Uint32 KeyEvent::getSanitizedMod() const { return mMod & KEYMOD_CTRL_SHIFT_ALT_META; } +bool TextInputEvent::isValidTextInputEvent( Input* input, const TextInputEvent& event ) { + // Meta/Command key shortcuts do not generate text + if ( input->isMetaPressed() ) + return false; + + // Ctrl shortcuts (without Alt/AltGr) do not generate text + if ( input->isLeftControlPressed() && !input->isLeftAltPressed() && !input->isAltGrPressed() ) + return false; + + // Alt+Tab should not insert a tab character + if ( input->isLeftAltPressed() && !event.getText().empty() && event.getText()[0] == '\t' ) + return false; + +#if EE_PLATFORM != EE_PLATFORM_MACOS + // On non-macOS platforms, Alt key combinations (without Ctrl) do not generate text + if ( input->isLeftAltPressed() && !input->isLeftControlPressed() ) + return false; +#endif + + return true; +} + +bool TextInputEvent::isValid( Input* input ) const { + return isValidTextInputEvent( input, *this ); +} + TextInputEvent::TextInputEvent( Node* node, const Uint32& eventNum, const Uint32& chr, const Uint32& timestamp ) : Event( node, eventNum ), mChar( chr ), mTimestamp( timestamp ) {} diff --git a/src/eepp/scene/scenemanager.cpp b/src/eepp/scene/scenemanager.cpp index e9c6bb97b..d1062213e 100644 --- a/src/eepp/scene/scenemanager.cpp +++ b/src/eepp/scene/scenemanager.cpp @@ -9,15 +9,13 @@ namespace EE { namespace Scene { SINGLETON_DECLARE_IMPLEMENTATION( SceneManager ) bool SceneManager::isActive() { - return EE::Window::Engine::isEngineRunning() && SceneManager::existsSingleton() && - !SceneManager::instance()->isShuttingDown(); + return Engine::isEngineRunning() && SceneManager::existsSingleton() && + !SceneManager::isShuttingDown(); } -SceneManager::SceneManager() : mUISceneNode( NULL ), mIsShuttingDown( false ) {} +SceneManager::SceneManager() : mUISceneNode( NULL ) {} SceneManager::~SceneManager() { - mIsShuttingDown = true; - for ( auto& it : mSceneNodes ) { SceneNode* node = it; eeSAFE_DELETE( node ); @@ -60,10 +58,6 @@ void SceneManager::update() { update( mClock.getElapsedTimeAndReset() ); } -bool SceneManager::isShuttingDown() const { - return mIsShuttingDown; -} - UISceneNode* SceneManager::getUISceneNode() { if ( NULL == mUISceneNode ) { for ( auto& sceneNode : mSceneNodes ) { diff --git a/src/eepp/system/color.cpp b/src/eepp/system/color.cpp index 8f89a2c51..c2d48a2cd 100644 --- a/src/eepp/system/color.cpp +++ b/src/eepp/system/color.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -18,6 +19,7 @@ template inline T _round( T r ) { } // namespace UnorderedMap Color::sColors; +UnorderedMap Color::sColorHash; UnorderedMap Color::sColorMap; // Keep the old defined colors @@ -686,10 +688,10 @@ Color Color::fromString( std::string str ) { return Color::Transparent; } } else if ( String::startsWith( str, "@color/" ) ) { - std::string colorName( String::toLower( str.substr( 7 ) ) ); - const auto& it = sColors.find( colorName ); + std::string_view colorName( std::string_view( str ).substr( 7 ) ); + const auto& it = sColorHash.find( String::hashToLower( colorName ) ); - if ( it != sColors.end() ) { + if ( it != sColorHash.end() ) { return it->second; } else { return Color::Transparent; @@ -701,10 +703,9 @@ Color Color::fromString( std::string str ) { if ( it != sColorMap.end() ) return it->second; } else { - String::toLowerInPlace( str ); - const auto& it = sColors.find( str ); + const auto& it = sColorHash.find( String::hashToLower( str ) ); - if ( it != sColors.end() ) { + if ( it != sColorHash.end() ) { return it->second; } else { return Color::Transparent; @@ -714,48 +715,122 @@ Color Color::fromString( std::string str ) { return Color::Transparent; } -bool Color::isColorString( std::string str ) { +template +bool Color::isColorStringT( StringType str, bool searchColorNames ) { if ( str.empty() ) return false; if ( str[0] == '#' ) - return true; + return validHexColorString( str ); - String::toLowerInPlace( str ); + if ( searchColorNames && str.size() <= 32 ) { + initColorMap(); - initColorMap(); - auto it = sColorMap.find( String::hash( str ) ); - if ( it != sColorMap.end() ) - return true; - if ( String::startsWith( str, "rgb(" ) ) - return true; - else if ( String::startsWith( str, "rgba(" ) ) - return true; - else if ( String::startsWith( str, "hsl(" ) ) - return true; - else if ( String::startsWith( str, "hsla(" ) ) - return true; - else if ( String::startsWith( str, "hsv(" ) ) - return true; - else if ( String::startsWith( str, "hsva(" ) ) - return true; - else if ( String::startsWith( str, "@color/" ) ) - return true; - else if ( sColors.find( str ) != sColors.end() ) - return true; + String::HashType hash = String::hashToLower( str ); + if ( sColorMap.find( hash ) != sColorMap.end() ) + return true; + + if ( sColorHash.find( hash ) != sColorHash.end() ) + return true; + } + + if ( String::istartsWith( str, "@color/" ) ) { + std::string utf8Str; + if constexpr ( std::is_same_v ) { + if ( LuaPattern::hasMatches( String( str ).toUtf8(), "@color/[%w_][%w_-]+" ) ) { + return true; + } + } else { + if ( LuaPattern::hasMatches( str, "@color/[%w_][%w_-]+" ) ) { + return true; + } + } + } + + if ( str.back() == ')' ) { + FunctionString func = FunctionString::parse( str ); + + if ( !func.isEmpty() ) { + std::string name = func.getName(); + String::toLowerInPlace( name ); + + if ( name == "rgb" || name == "rgba" || name == "hsl" || name == "hsla" || + name == "hsv" || name == "hsva" ) { + + const auto& params = func.getParameters(); + if ( params.empty() || params.size() > 4 ) + return false; + + for ( const auto& param : params ) { + if ( param.empty() ) + return false; + + bool hasFunc = false; + static const char* allowedFuncs[] = { + "var(", "calc(", "min(", "max(", "clamp(", "env(", "color-mix(", "color(" }; + + for ( const char* f : allowedFuncs ) { + if ( param.find( f ) != std::string::npos ) { + hasFunc = true; + break; + } + } + + if ( hasFunc ) + continue; + + for ( auto c : param ) { + if ( !( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || + ( c >= '0' && c <= '9' ) || c == '.' || c == '%' || c == '-' || + c == '+' || c == ' ' || c == '/' ) ) { + return false; + } + } + } + + return true; + } + } + } return false; } +bool Color::isColorString( std::string_view str, bool searchColorNames ) { + return isColorStringT( str, searchColorNames ); +} + +bool Color::isColorString( String::View str, bool searchColorNames ) { + return isColorStringT( str, searchColorNames ); +} + void Color::registerColor( const std::string& name, const Color& color ) { - sColors[String::toLower( name )] = color; + std::string lowerName( String::toLower( name ) ); + sColors[lowerName] = color; + sColorHash[String::hash( lowerName )] = color; } bool Color::unregisterColor( const std::string& name ) { - return sColors.erase( String::toLower( name ) ) > 0; + std::string lowerName( String::toLower( name ) ); + sColorHash.erase( String::hash( lowerName ) ); + return sColors.erase( lowerName ) > 0; } -bool Color::validHexColorString( const std::string& hexColor ) { +bool Color::validHexColorString( String::View hexColor ) { + if ( hexColor.size() < 2 || hexColor[0] != '#' ) + return false; + + for ( size_t i = 1; i < hexColor.size(); i++ ) { + if ( !( String::isNumber( hexColor[i] ) || ( hexColor[i] >= 'a' && hexColor[i] <= 'f' ) || + ( hexColor[i] >= 'A' && hexColor[i] <= 'F' ) ) ) { + return false; + } + } + + return true; +} + +bool Color::validHexColorString( std::string_view hexColor ) { if ( hexColor.size() < 2 || hexColor[0] != '#' ) return false; diff --git a/src/eepp/system/compression.cpp b/src/eepp/system/compression.cpp index 652b6b530..cd7d09981 100644 --- a/src/eepp/system/compression.cpp +++ b/src/eepp/system/compression.cpp @@ -3,6 +3,9 @@ #include #include +#include +// eepp only brings decoder implementation, encoding won't be available for the moment +// #include #include #define DEFLATE_CHUNK_SIZE ( 16384 ) @@ -19,6 +22,62 @@ Compression::Status Compression::compress( Uint8* dst, Uint64 dstMaxSize, const Compression::Status Compression::compress( IOStream& dst, IOStream& src, Compression::Mode mode, const Config& config ) { switch ( mode ) { + case MODE_BROTLI: { +/* + BrotliEncoderState* state = BrotliEncoderCreateInstance( nullptr, nullptr, nullptr ); + if ( !state ) + return Status::MEM_ERROR; + + int quality = + config.brotli.quality == -1 ? BROTLI_DEFAULT_QUALITY : config.brotli.quality; + int windowBits = + config.brotli.windowBits == -1 ? BROTLI_DEFAULT_WINDOW : config.brotli.windowBits; + + BrotliEncoderSetParameter( state, BROTLI_PARAM_QUALITY, quality ); + BrotliEncoderSetParameter( state, BROTLI_PARAM_LGWIN, windowBits ); + + src.seek( 0 ); + + char in[DEFLATE_CHUNK_SIZE]; + char out[DEFLATE_CHUNK_SIZE]; + + bool isEof = false; + + while ( !isEof ) { + size_t bytesRead = src.read( in, DEFLATE_CHUNK_SIZE ); + isEof = src.tell() == src.getSize(); + + const uint8_t* next_in = reinterpret_cast( in ); + size_t avail_in = bytesRead; + + while ( avail_in > 0 || ( isEof && !BrotliEncoderIsFinished( state ) ) ) { + uint8_t* next_out = reinterpret_cast( out ); + size_t avail_out = DEFLATE_CHUNK_SIZE; + + BrotliEncoderOperation op = + isEof ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; + + if ( !BrotliEncoderCompressStream( state, op, &avail_in, &next_in, &avail_out, + &next_out, nullptr ) ) { + BrotliEncoderDestroyInstance( state ); + return Status::STREAM_ERROR; + } + + size_t have = DEFLATE_CHUNK_SIZE - avail_out; + if ( have > 0 ) { + if ( dst.write( out, have ) != (ios_size)have ) { + BrotliEncoderDestroyInstance( state ); + return Status::ERRNO; + } + } + } + } + + BrotliEncoderDestroyInstance( state ); + return Status::OK; +*/ + return Status::VERSION_ERROR; + } case MODE_DEFLATE: case MODE_GZIP: { int ret, flush; @@ -74,6 +133,10 @@ Compression::Status Compression::compress( IOStream& dst, IOStream& src, Compres int Compression::getMaxCompressedBufferSize( Uint64 srcSize, Mode mode, const Config& ) { switch ( mode ) { + case MODE_BROTLI: { + // return BrotliEncoderMaxCompressedSize( srcSize ); + break; + } case MODE_DEFLATE: case MODE_GZIP: { int windowBits = mode == MODE_DEFLATE ? MAX_WBITS : MAX_WBITS | 16; @@ -101,6 +164,61 @@ Compression::Status Compression::decompress( Uint8* dst, Uint64 dstMaxSize, cons Compression::Status Compression::decompress( IOStream& dst, IOStream& src, Mode mode ) { switch ( mode ) { + case MODE_BROTLI: { + BrotliDecoderState* state = BrotliDecoderCreateInstance( nullptr, nullptr, nullptr ); + if ( !state ) + return Status::MEM_ERROR; + + ScopedBuffer buffer( DEFLATE_CHUNK_SIZE ); + ScopedBuffer bufferDst( DEFLATE_CHUNK_SIZE ); + + src.seek( 0 ); + + BrotliDecoderResult result = BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; + size_t totalSize = src.getSize(); + size_t totalRead = 0; + + while ( totalRead < totalSize || result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT ) { + size_t bytesRead = 0; + if ( result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT ) { + bytesRead = src.read( (char*)buffer.get(), buffer.length() ); + totalRead += bytesRead; + } + + const uint8_t* next_in = reinterpret_cast( buffer.get() ); + size_t avail_in = bytesRead; + + while ( avail_in > 0 || result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT ) { + uint8_t* next_out = reinterpret_cast( bufferDst.get() ); + size_t avail_out = bufferDst.length(); + + result = BrotliDecoderDecompressStream( state, &avail_in, &next_in, &avail_out, + &next_out, nullptr ); + + if ( result == BROTLI_DECODER_RESULT_ERROR ) { + BrotliDecoderDestroyInstance( state ); + return Status::DATA_ERROR; + } + + size_t have = bufferDst.length() - avail_out; + if ( have > 0 ) { + dst.write( (const char*)bufferDst.get(), have ); + } + + if ( result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT || + result == BROTLI_DECODER_RESULT_SUCCESS ) { + break; + } + } + + if ( result == BROTLI_DECODER_RESULT_SUCCESS ) { + break; + } + } + + BrotliDecoderDestroyInstance( state ); + return Status::OK; + } case MODE_DEFLATE: case MODE_GZIP: { ScopedBuffer buffer( DEFLATE_CHUNK_SIZE ); diff --git a/src/eepp/system/filesystem.cpp b/src/eepp/system/filesystem.cpp index 8b2f348b5..0048756ad 100644 --- a/src/eepp/system/filesystem.cpp +++ b/src/eepp/system/filesystem.cpp @@ -227,6 +227,31 @@ Uint32 FileSystem::fileGetModificationDate( const std::string& filepath ) { return 0; } +size_t FileSystem::fileCountLines( const std::string& path, bool* isBinary ) { + if ( !fileExists( path ) ) + return 0; + IOStreamFile fs( path ); + if ( !fs.isOpen() || fs.getSize() == 0 ) + return 0; + size_t count = 1; + char buffer[65536]; + ios_size read; + if ( isBinary ) + *isBinary = false; + while ( ( read = fs.read( buffer, sizeof( buffer ) ) ) > 0 ) { + for ( ios_size i = 0; i < read; ++i ) { + if ( buffer[i] == '\0' ) { + if ( isBinary ) + *isBinary = true; + return 0; + } + if ( buffer[i] == '\n' ) + count++; + } + } + return count; +} + bool FileSystem::fileCanWrite( const std::string& filepath ) { #if EE_PLATFORM == EE_PLATFORM_WIN auto attrs = GetFileAttributesW( String::fromUtf8( filepath ).toWideString().c_str() ); diff --git a/src/eepp/system/functionstring.cpp b/src/eepp/system/functionstring.cpp index 924c554ac..5649ca71c 100644 --- a/src/eepp/system/functionstring.cpp +++ b/src/eepp/system/functionstring.cpp @@ -4,50 +4,69 @@ namespace EE { namespace System { -FunctionString FunctionString::parse( const std::string& function ) { +#include +#include + +template +FunctionString FunctionString::parse( StringType function ) { + using CharType = typename StringType::value_type; + size_t funcSep = function.find( '(' ); - if ( funcSep == std::string::npos ) + if ( funcSep == StringType::npos ) return FunctionString( "", {}, {} ); - std::string funcName = function.substr( 0, funcSep ); - String::trimInPlace( funcName ); + auto funcName = String::trim( function.substr( 0, funcSep ) ); + Parameters funcParameters; + TypeStringVector typeStringData; - std::vector funcParameters; - std::vector typeStringData; - - std::string parametersString = function.substr( funcSep + 1 ); + auto parametersString = function.substr( funcSep + 1 ); size_t paramClose = parametersString.find_last_of( ')' ); - if ( paramClose == std::string::npos ) + if ( paramClose == StringType::npos ) return FunctionString( "", {}, {} ); parametersString = parametersString.substr( 0, paramClose ); bool stateParsingString = false; - std::string buffer = ""; - char prevChar = 0; + std::basic_string buffer; + CharType prevChar = 0; + bool currentParamIsString = false; int parenDepth = 0; + auto pushBufferToParams = [&]() { + if constexpr ( std::same_as ) { + if ( !currentParamIsString ) + String::trimInPlace( buffer ); + funcParameters.push_back( buffer ); + } else { + std::string utf8Buffer = String( buffer ).toUtf8(); + if ( !currentParamIsString ) + String::trimInPlace( utf8Buffer ); + funcParameters.push_back( utf8Buffer ); + } + typeStringData.push_back( currentParamIsString ); + buffer.clear(); + currentParamIsString = false; + }; + for ( size_t i = 0; i < parametersString.length(); ++i ) { - char c = parametersString[i]; + + CharType c = parametersString[i]; if ( !stateParsingString ) { if ( c == '(' ) { parenDepth++; buffer += c; } else if ( c == ')' ) { + if ( parenDepth == 0 ) + break; if ( parenDepth > 0 ) parenDepth--; buffer += c; } else if ( c == ',' ) { if ( parenDepth == 0 ) { if ( !buffer.empty() ) { - if ( !currentParamIsString ) - String::trimInPlace( buffer ); - funcParameters.push_back( buffer ); - typeStringData.push_back( currentParamIsString ); - buffer = ""; - currentParamIsString = false; + pushBufferToParams(); } } else { buffer += c; @@ -82,25 +101,39 @@ FunctionString FunctionString::parse( const std::string& function ) { } } - if ( !buffer.empty() ) { - if ( !currentParamIsString ) - String::trimInPlace( buffer ); - funcParameters.push_back( buffer ); - typeStringData.push_back( currentParamIsString ); - } + if ( !buffer.empty() ) + pushBufferToParams(); - return FunctionString( funcName, funcParameters, typeStringData ); + if constexpr ( std::same_as ) { + return FunctionString( std::string{ funcName }, funcParameters, typeStringData ); + } else { + return FunctionString( String( funcName ).toUtf8(), funcParameters, typeStringData ); + } } -FunctionString::FunctionString( const std::string& name, const std::vector& parameters, - const std::vector& typeStringData ) : +FunctionString FunctionString::parse( std::string_view function ) { + return parse( function ); +} + +FunctionString FunctionString::parse( String::View function ) { + return parse( function ); +} + +FunctionString::FunctionString( const std::string& name, const Parameters& parameters, + const TypeStringVector& typeStringData ) : name( name ), parameters( parameters ), typeStringData( typeStringData ) {} +FunctionString::FunctionString( const std::string& name, Parameters&& parameters, + TypeStringVector&& typeStringData ) : + name( name ), + parameters( std::move( parameters ) ), + typeStringData( std::move( typeStringData ) ) {} + const std::string& FunctionString::getName() const { return name; } -const std::vector& FunctionString::getParameters() const { +const FunctionString::Parameters& FunctionString::getParameters() const { return parameters; } diff --git a/src/eepp/system/iostreamdeflate.cpp b/src/eepp/system/iostreamdeflate.cpp index 6e53c9dfc..63a0856da 100644 --- a/src/eepp/system/iostreamdeflate.cpp +++ b/src/eepp/system/iostreamdeflate.cpp @@ -4,7 +4,7 @@ namespace EE { namespace System { -struct LocalStreamData { +struct LocalDeflateStreamData { z_stream strm; int state; bool writtenStream; @@ -20,7 +20,7 @@ IOStreamDeflate::IOStreamDeflate( IOStream& inOutStream, Compression::Mode mode, mStream( inOutStream ), mMode( mode ), mBuffer( Compression::getModeDefaultChunkSize( mode ) ), - mLocalStream( eeNew( LocalStreamData, () ) ) { + mLocalStream( eeNew( LocalDeflateStreamData, () ) ) { int windowBits = mode == Compression::MODE_DEFLATE ? MAX_WBITS : MAX_WBITS | 16; int level = mode == Compression::MODE_DEFLATE ? config.zlib.level : config.gzip.level; diff --git a/src/eepp/system/iostreaminflate.cpp b/src/eepp/system/iostreaminflate.cpp index b01fbd7ba..65ee13e05 100644 --- a/src/eepp/system/iostreaminflate.cpp +++ b/src/eepp/system/iostreaminflate.cpp @@ -1,12 +1,17 @@ #include +#include #include namespace EE { namespace System { -struct LocalStreamData { +struct LocalInflateStreamData { z_stream strm; int state; + BrotliDecoderState* brotliState; + BrotliDecoderResult brotliResult; + size_t brotliAvailIn; + const uint8_t* brotliNextIn; }; IOStreamInflate* IOStreamInflate::New( IOStream& inOutStream, Compression::Mode mode ) { @@ -17,24 +22,71 @@ IOStreamInflate::IOStreamInflate( IOStream& inOutStream, Compression::Mode mode mStream( inOutStream ), mMode( mode ), mBuffer( Compression::getModeDefaultChunkSize( mode ) ), - mLocalStream( eeNew( LocalStreamData, () ) ) { - int windowBits = mode == Compression::MODE_DEFLATE ? MAX_WBITS : MAX_WBITS | 16; + mLocalStream( eeNew( LocalInflateStreamData, () ) ) { - mLocalStream->strm = z_stream{}; - - mLocalStream->state = inflateInit2( &mLocalStream->strm, windowBits ); + if ( mode == Compression::MODE_BROTLI ) { + mLocalStream->brotliState = BrotliDecoderCreateInstance( nullptr, nullptr, nullptr ); + mLocalStream->brotliResult = BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; + mLocalStream->brotliAvailIn = 0; + mLocalStream->brotliNextIn = nullptr; + mLocalStream->state = mLocalStream->brotliState ? Z_OK : Z_MEM_ERROR; + } else { + int windowBits = mode == Compression::MODE_DEFLATE ? MAX_WBITS : MAX_WBITS | 16; + mLocalStream->strm = z_stream{}; + mLocalStream->state = inflateInit2( &mLocalStream->strm, windowBits ); + } } IOStreamInflate::~IOStreamInflate() { - inflateEnd( &mLocalStream->strm ); + if ( mMode == Compression::MODE_BROTLI ) { + if ( mLocalStream->brotliState ) + BrotliDecoderDestroyInstance( mLocalStream->brotliState ); + } else { + inflateEnd( &mLocalStream->strm ); + } eeSAFE_DELETE( mLocalStream ); } ios_size IOStreamInflate::read( char* buffer, ios_size length ) { - if ( mLocalStream->state != Z_OK || !mStream.isOpen() ) + if ( mLocalStream->state != Z_OK || !mStream.isOpen() || length == 0 ) return 0; + if ( mMode == Compression::MODE_BROTLI ) { + size_t avail_out = length; + uint8_t* next_out = reinterpret_cast( buffer ); + + while ( avail_out > 0 ) { + if ( mLocalStream->brotliAvailIn == 0 && + mLocalStream->brotliResult != BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT ) { + ios_size n = 0; + if ( mStream.isOpen() ) { + n = mStream.read( (char*)mBuffer.get(), mBuffer.length() ); + } + if ( n == 0 ) + break; + mLocalStream->brotliAvailIn = n; + mLocalStream->brotliNextIn = reinterpret_cast( mBuffer.get() ); + } + + mLocalStream->brotliResult = BrotliDecoderDecompressStream( + mLocalStream->brotliState, &mLocalStream->brotliAvailIn, + &mLocalStream->brotliNextIn, &avail_out, &next_out, nullptr ); + + if ( mLocalStream->brotliResult == BROTLI_DECODER_RESULT_ERROR ) { + mLocalStream->state = Z_DATA_ERROR; + return 0; + } + + if ( mLocalStream->brotliResult == BROTLI_DECODER_RESULT_SUCCESS ) { + mLocalStream->state = Z_STREAM_END; + break; + } + } + + return length - avail_out; + } + z_stream& zstr = mLocalStream->strm; if ( zstr.avail_in == 0 ) { @@ -94,6 +146,43 @@ ios_size IOStreamInflate::write( const char* buffer, ios_size length ) { if ( mLocalStream->state != Z_OK || !mStream.isOpen() || length == 0 ) return 0; + if ( mMode == Compression::MODE_BROTLI ) { + size_t avail_in = length; + const uint8_t* next_in = reinterpret_cast( buffer ); + + while ( avail_in > 0 || + mLocalStream->brotliResult == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT ) { + size_t avail_out = mBuffer.length(); + uint8_t* next_out = reinterpret_cast( mBuffer.get() ); + + mLocalStream->brotliResult = BrotliDecoderDecompressStream( + mLocalStream->brotliState, &avail_in, &next_in, &avail_out, &next_out, nullptr ); + + if ( mLocalStream->brotliResult == BROTLI_DECODER_RESULT_ERROR ) { + mLocalStream->state = Z_DATA_ERROR; + return 0; + } + + size_t have = mBuffer.length() - avail_out; + if ( have > 0 ) { + if ( mStream.write( (const char*)mBuffer.get(), have ) != (ios_size)have ) { + return 0; + } + } + + if ( mLocalStream->brotliResult == BROTLI_DECODER_RESULT_SUCCESS ) { + mLocalStream->state = Z_STREAM_END; + break; + } + + if ( mLocalStream->brotliResult == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT ) { + break; + } + } + + return length; + } + z_stream& zstr = mLocalStream->strm; zstr.next_in = (unsigned char*)buffer; diff --git a/src/eepp/system/luapattern.cpp b/src/eepp/system/luapattern.cpp index 987f61ff2..3fb851824 100644 --- a/src/eepp/system/luapattern.cpp +++ b/src/eepp/system/luapattern.cpp @@ -35,16 +35,16 @@ std::string_view LuaPattern::getURIPattern() { return "%w+://[%w_.~!*:@&+$/?%%#-]-%w[-.%w]*%.%w%w%w?%w?:?%d*/?[%w_.~!*:@&+$/?%%#=-]*"sv; } -std::string LuaPattern::match( const std::string& string, const std::string_view& pattern ) { +std::string LuaPattern::match( std::string_view string, std::string_view pattern ) { LuaPattern matcher( pattern ); int start = 0, end = 0; - if ( matcher.find( string, start, end ) ) - return string.substr( start, end - start ); + if ( matcher.find( string.data(), start, end, 0, string.size() ) ) + return std::string{ string.substr( start, end - start ) }; return ""; } std::string LuaPattern::matchesAny( const std::vector& stringvec, - const std::string_view& pattern ) { + std::string_view pattern ) { LuaPattern matcher( pattern ); int start = 0, end = 0; for ( const auto& str : stringvec ) { @@ -55,16 +55,15 @@ std::string LuaPattern::matchesAny( const std::vector& stringvec, return ""; } -PatternMatcher::Range LuaPattern::firstMatch( const std::string& string, - const std::string_view& pattern ) { +PatternMatcher::Range LuaPattern::firstMatch( std::string_view string, std::string_view pattern ) { LuaPattern matcher( pattern ); int start = 0, end = 0; - if ( matcher.find( string, start, end ) ) + if ( matcher.find( string.data(), start, end, 0, string.size() ) ) return { start, end }; return { -1, -1 }; } -bool LuaPattern::hasMatches( const std::string& string, const std::string_view& pattern ) { +bool LuaPattern::hasMatches( std::string_view string, std::string_view pattern ) { return LuaPattern::firstMatch( string, pattern ).isValid(); } diff --git a/src/eepp/ui/abstract/uiabstracttableview.cpp b/src/eepp/ui/abstract/uiabstracttableview.cpp index 8f69a8137..2b8e2eb29 100644 --- a/src/eepp/ui/abstract/uiabstracttableview.cpp +++ b/src/eepp/ui/abstract/uiabstracttableview.cpp @@ -111,6 +111,19 @@ void UIAbstractTableView::selectAll() { } } +std::vector UIAbstractTableView::getSelectionRange( const ModelIndex& start, + const ModelIndex& end ) const { + std::vector range; + if ( !getModel() ) + return range; + int minRow = eemin( start.row(), end.row() ); + int maxRow = eemax( start.row(), end.row() ); + for ( int i = minRow; i <= maxRow; ++i ) { + range.push_back( getModel()->index( i, start.column() ) ); + } + return range; +} + size_t UIAbstractTableView::getItemCount() const { if ( !getModel() ) return 0; @@ -507,17 +520,34 @@ UITableRow* UIAbstractTableView::createRow() { return; auto index = event->getNode()->asType()->getCurIndex(); if ( mSelectionKind == SelectionKind::Single && - getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) { + ( getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) ) { getSelection().remove( index ); } else { if ( mSelectionKind == SelectionKind::Multiple && - getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) { + ( getInput()->getSanitizedModState() & KeyMod::getDefaultModifier() ) ) { getSelection().toggle( index ); + } else if ( mSelectionKind == SelectionKind::Multiple && + ( getInput()->getSanitizedModState() & KEYMOD_SHIFT ) && + !getSelection().isEmpty() ) { + getSelection().set( getSelectionRange( getSelection().first(), index ) ); + } else if ( mSelectionKind == SelectionKind::Multiple ) { + if ( !getSelection().contains( index ) ) + getSelection().set( index ); } else { getSelection().set( index ); } } } ); + rowWidget->on( Event::MouseClick, [this]( const Event* event ) { + if ( !( event->asMouseEvent()->getFlags() & ( EE_BUTTON_LMASK ) ) || + !isRowSelection() ) + return; + + auto index = event->getNode()->asType()->getCurIndex(); + if ( 0 == getInput()->getSanitizedModState() ) { + getSelection().set( index ); + } + } ); onRowCreated( rowWidget ); return rowWidget; } diff --git a/src/eepp/ui/css/drawableimageparser.cpp b/src/eepp/ui/css/drawableimageparser.cpp index af0326aa4..06f852692 100644 --- a/src/eepp/ui/css/drawableimageparser.cpp +++ b/src/eepp/ui/css/drawableimageparser.cpp @@ -36,7 +36,8 @@ Drawable* DrawableImageParser::createDrawable( const std::string& value, const S if ( !functionType.isEmpty() ) { if ( exists( functionType.getName() ) ) return mFuncs[functionType.getName()]( functionType, size, ownIt, node ); - } else if ( NULL != ( res = DrawableSearcher::searchByName( value ) ) ) { + } else if ( NULL != ( res = DrawableSearcher::searchByName( + value, false, node->getUISceneNode()->getReferer() ) ) ) { if ( res->getDrawableType() == Drawable::SPRITE ) ownIt = true; return res; @@ -66,7 +67,7 @@ void DrawableImageParser::registerBaseParsers() { RectangleDrawable* drawable = RectangleDrawable::New(); RectColors rectColors; - const std::vector& params( functionType.getParameters() ); + const auto& params( functionType.getParameters() ); if ( Color::isColorString( params.at( 0 ) ) && params.size() >= 2 ) { rectColors.TopLeft = rectColors.TopRight = Color::fromString( params.at( 0 ) ); @@ -112,7 +113,7 @@ void DrawableImageParser::registerBaseParsers() { CircleDrawable* drawable = CircleDrawable::New(); - const std::vector& params( functionType.getParameters() ); + const auto& params( functionType.getParameters() ); CSS::StyleSheetLength length( params[0] ); drawable->setRadius( node->convertLength( length, size.getWidth() / 2.f ) ); @@ -145,7 +146,7 @@ void DrawableImageParser::registerBaseParsers() { RectColors rectColors; std::vector colors; - const std::vector& params( functionType.getParameters() ); + const auto& params( functionType.getParameters() ); for ( size_t i = 0; i < params.size(); i++ ) { std::string param( String::toLower( params[i] ) ); @@ -207,7 +208,7 @@ void DrawableImageParser::registerBaseParsers() { std::vector colors; std::vector vertices; - const std::vector& params( functionType.getParameters() ); + const auto& params( functionType.getParameters() ); Float lineWidth = PixelDensity::dpToPx( 1.f ); for ( size_t i = 0; i < params.size(); i++ ) { @@ -277,7 +278,7 @@ void DrawableImageParser::registerBaseParsers() { std::vector colors; std::vector vertices; - const std::vector& params( functionType.getParameters() ); + const auto& params( functionType.getParameters() ); Float lineWidth = PixelDensity::dpToPx( 1.f ); for ( size_t i = 0; i < params.size(); i++ ) { @@ -327,12 +328,15 @@ void DrawableImageParser::registerBaseParsers() { }; mFuncs["url"] = []( const FunctionString& functionType, const Sizef& /*size*/, bool& /*ownIt*/, - UINode* - /*node*/ ) -> Drawable* { + UINode* node ) -> Drawable* { if ( functionType.getParameters().size() < 1 ) return NULL; - return DrawableSearcher::searchByName( functionType.getParameters().at( 0 ) ); + return DrawableSearcher::searchByName( + node->getUISceneNode() + ->solveRelativePath( functionType.getParameters().at( 0 ) ) + .toString(), + false, node->getUISceneNode()->getReferer() ); }; mFuncs["icon"] = []( const FunctionString& functionType, const Sizef& size, bool&, diff --git a/src/eepp/ui/css/mediaquery.cpp b/src/eepp/ui/css/mediaquery.cpp index dff7d4182..974a7e92e 100644 --- a/src/eepp/ui/css/mediaquery.cpp +++ b/src/eepp/ui/css/mediaquery.cpp @@ -94,14 +94,7 @@ MediaQuery::ptr MediaQuery::parse( const std::string& str ) { StyleSheetLength length = StyleSheetLength::fromString( exprTokens[1] ); expr.valStr = String::toLower( exprTokens[1] ); - - if ( length.getUnit() == StyleSheetLength::Unit::Dpcm || - length.getUnit() == StyleSheetLength::Unit::Dpi ) { - expr.val = (int)( length.getValue() * 2.54 ); - } else { - expr.val = (int)length.asPixels( 0, Sizef::Zero, dpi ); - } - + expr.val = (int)length.asPixels( 0, Sizef::Zero, dpi ); expr.fval = length.getValue(); } } diff --git a/src/eepp/ui/css/propertyspecification.cpp b/src/eepp/ui/css/propertyspecification.cpp index ee0e9f8c5..4ccc485df 100644 --- a/src/eepp/ui/css/propertyspecification.cpp +++ b/src/eepp/ui/css/propertyspecification.cpp @@ -40,6 +40,10 @@ const SmallVector& PropertySpecification::getInheritableProperties() return mInheritableProperties; } +const PropertyDefinition* PropertySpecification::getProperty( const PropertyId& id ) const { + return getProperty( static_cast>( id ) ); +} + const PropertyDefinition* PropertySpecification::getProperty( const Uint32& id ) const { auto it = mProperties.find( id ); diff --git a/src/eepp/ui/css/stylesheet.cpp b/src/eepp/ui/css/stylesheet.cpp index ee441fb4c..2fcf19bb5 100644 --- a/src/eepp/ui/css/stylesheet.cpp +++ b/src/eepp/ui/css/stylesheet.cpp @@ -57,54 +57,40 @@ void StyleSheet::setMarker( const Uint32& marker ) { } void StyleSheet::removeAllWithMarker( const Uint32& marker ) { - std::vector> removeNodes; + std::erase_if( mNodeIndex, [marker]( auto& pair ) { + std::erase_if( pair.second, + [marker]( const auto& node ) { return node->getMarker() == marker; } ); + return pair.second.empty(); // If true, the map entry is erased + } ); - for ( auto& node : mNodes ) - if ( node->getMarker() == marker ) - removeNodes.emplace_back( node ); + std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() == marker; } ); - std::vector deprecatedNodeIndex; - for ( auto& nodeIndex : mNodeIndex ) { - std::vector removeNodesIndex; - for ( auto node : nodeIndex.second ) { - if ( node->getMarker() == marker ) { - removeNodesIndex.emplace_back( node ); - } - } - for ( auto removeNodeIndex : removeNodesIndex ) { - auto found = - std::find( nodeIndex.second.begin(), nodeIndex.second.end(), removeNodeIndex ); - if ( found != nodeIndex.second.end() ) - nodeIndex.second.erase( found ); - } - if ( nodeIndex.second.empty() ) - deprecatedNodeIndex.emplace_back( nodeIndex.first ); - } + std::erase_if( mMediaQueryList, [marker]( const auto& mediaQueryList ) { + return mediaQueryList->getMarker() == marker; + } ); - for ( auto removeIndex : deprecatedNodeIndex ) - mNodeIndex.erase( removeIndex ); + std::erase_if( mKeyframesMap, + [marker]( const auto& pair ) { return pair.second.getMarker() == marker; } ); - std::vector removeMediaQueries; - for ( auto& mediaQueryList : mMediaQueryList ) { - if ( mediaQueryList->getMarker() == marker ) - removeMediaQueries.emplace_back( mediaQueryList ); - } - if ( !removeMediaQueries.empty() ) { - for ( auto& removeMediaQuery : removeMediaQueries ) { - auto found = - std::find( mMediaQueryList.begin(), mMediaQueryList.end(), removeMediaQuery ); - if ( found != mMediaQueryList.end() ) - mMediaQueryList.erase( found ); - } - } + invalidateCache(); +} - std::vector removeKeys; - for ( auto& keyFrame : mKeyframesMap ) { - if ( keyFrame.second.getMarker() == marker ) - removeKeys.emplace_back( keyFrame.first ); - } - for ( auto& removeKey : removeKeys ) - mKeyframesMap.erase( removeKey ); +void StyleSheet::removeAllWithoutMarker( const Uint32& marker ) { + std::erase_if( mNodeIndex, [marker]( auto& pair ) { + std::erase_if( pair.second, [marker]( const auto& node ) { + return node->getMarker() != marker; // Notice the != + } ); + return pair.second.empty(); + } ); + + std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() != marker; } ); + + std::erase_if( mMediaQueryList, [marker]( const auto& mediaQueryList ) { + return mediaQueryList->getMarker() != marker; + } ); + + std::erase_if( mKeyframesMap, + [marker]( const auto& pair ) { return pair.second.getMarker() != marker; } ); invalidateCache(); } @@ -371,16 +357,11 @@ StyleSheetStyleVector StyleSheet::getStyleSheetStyleByAtRule( const AtRuleType& if ( node->getAtRuleType() == atRuleType ) vector.push_back( node.get() ); - std::sort( vector.begin(), vector.end(), - []( const StyleSheetStyle* left, const StyleSheetStyle* right ) { - bool leftHasIt = left->hasProperty( PropertyId::FontStyle ); - bool rightHasIt = right->hasProperty( PropertyId::FontStyle ); - if ( leftHasIt && !rightHasIt ) - return false; - if ( !leftHasIt && rightHasIt ) - return true; - return leftHasIt && rightHasIt; - } ); + std::sort( vector.begin(), vector.end(), []( const auto& left, const auto& right ) { + bool leftHasIt = left->hasProperty( PropertyId::FontStyle ); + bool rightHasIt = right->hasProperty( PropertyId::FontStyle ); + return leftHasIt < rightHasIt; + } ); return vector; } diff --git a/src/eepp/ui/css/stylesheetlength.cpp b/src/eepp/ui/css/stylesheetlength.cpp index 96368328c..76e47e948 100644 --- a/src/eepp/ui/css/stylesheetlength.cpp +++ b/src/eepp/ui/css/stylesheetlength.cpp @@ -1,7 +1,7 @@ -#include #include #include #include +#include #include using namespace EE::Graphics; @@ -30,6 +30,7 @@ enum UnitHashes : String::HashType { Dprd = String::hash( "dprd" ), Dpru = String::hash( "dpru" ), Dpr = String::hash( "dpr" ), + Ch = String::hash( "ch" ), }; enum PercentagePositions : String::HashType { @@ -116,6 +117,8 @@ StyleSheetLength::Unit StyleSheetLength::unitFromString( std::string unitStr ) { return Unit::Dpru; case UnitHashes::Dpr: return Unit::Dpr; + case UnitHashes::Ch: + return Unit::Ch; } return Unit::Dp; } @@ -162,6 +165,8 @@ std::string StyleSheetLength::unitToString( const StyleSheetLength::Unit& unit ) return "dpru"; case Unit::Dpr: return "dpr"; + case Unit::Ch: + return "ch"; } return "px"; } @@ -213,6 +218,11 @@ Float StyleSheetLength::asPixels( const Float& parentSize, const Sizef& viewSize const Float& displayDpi, const Float& elFontSize, const Float& globalFontSize ) const { Float ret = 0; + + // CSS dictates a base 96 DPI for logical pixels. + // We multiply by the device pixel ratio to get actual physical pixels on screen. + const Float CSS_DPI = 96.f * PixelDensity::getPixelDensity(); + switch ( mUnit ) { case Unit::Percentage: ret = parentSize * mValue / 100.f; @@ -229,22 +239,23 @@ Float StyleSheetLength::asPixels( const Float& parentSize, const Sizef& viewSize ret = Math::roundUp( PixelDensity::dpToPx( mValue ) ); break; case Unit::Em: + case Unit::Ch: // Using Em for Ch is incorrect but not that incorrect, close enough ret = Math::round( mValue * elFontSize ); break; case Unit::Pt: - ret = ( mValue * displayDpi / 72.f ); + ret = mValue * CSS_DPI / 72.f; break; case Unit::Pc: - ret = ( mValue * displayDpi / 72.f ) * 12.f; + ret = ( mValue * CSS_DPI / 72.f ) * 12.f; break; case Unit::In: - ret = mValue * displayDpi; + ret = mValue * CSS_DPI; break; case Unit::Cm: - ret = mValue * displayDpi * 0.3937f; + ret = ( mValue * CSS_DPI ) / 2.54f; break; case Unit::Mm: - ret = mValue * displayDpi * 0.3937f / 10.f; + ret = ( mValue * CSS_DPI ) / 25.4f; break; case Unit::Vw: ret = viewSize.getWidth() * mValue / 100.f; @@ -261,6 +272,11 @@ Float StyleSheetLength::asPixels( const Float& parentSize, const Sizef& viewSize case Unit::Rem: ret = globalFontSize * mValue; break; + case Unit::Dpi: + case Unit::Dpcm: + ret = (int)( mValue * 2.54 ); + break; + case Unit::Px: default: ret = mValue; break; @@ -295,7 +311,8 @@ StyleSheetLength& StyleSheetLength::operator=( const StyleSheetLength& val ) { return *this; } -StyleSheetLength StyleSheetLength::fromString( const std::string& str, const Float& defaultValue ) { +StyleSheetLength StyleSheetLength::fromString( const std::string& str, const Float& defaultValue, + bool pxAsDp ) { PercentagePositions isPercentage = isPercentagePosition( String::hash( str ) ); if ( PercentagePositions::None != isPercentage ) return fromString( positionToPercentage( isPercentage ), defaultValue ); @@ -321,6 +338,9 @@ StyleSheetLength StyleSheetLength::fromString( const std::string& str, const Flo length.setValue( val, unitFromString( unit ) ); } + if ( pxAsDp && length.getUnit() == Unit::Px ) + length.mUnit = Unit::Dp; + return length; } diff --git a/src/eepp/ui/css/stylesheetproperty.cpp b/src/eepp/ui/css/stylesheetproperty.cpp index f88a3fb84..632f5a0be 100644 --- a/src/eepp/ui/css/stylesheetproperty.cpp +++ b/src/eepp/ui/css/stylesheetproperty.cpp @@ -685,4 +685,9 @@ bool StyleSheetProperty::isCachedProperty() const { return mCachedProperty; } +void StyleSheetProperty::setImportant( bool important ) { + mImportant = important; + mSpecificity = StyleSheetSelectorRule::SpecificityImportant; +} + }}} // namespace EE::UI::CSS diff --git a/src/eepp/ui/css/stylesheetselectorrule.cpp b/src/eepp/ui/css/stylesheetselectorrule.cpp index 15e9c6f54..8e0872b73 100644 --- a/src/eepp/ui/css/stylesheetselectorrule.cpp +++ b/src/eepp/ui/css/stylesheetselectorrule.cpp @@ -14,8 +14,9 @@ static int numberOfSetBits( Uint32 i ) { // than uint32_t) } -static const char* StatePseudoClasses[] = { "focus", "selected", "hover", "pressed", - "disabled", "focus-within", "active" }; +static const char* StatePseudoClasses[] = { "focus", "selected", "hover", + "pressed", "disabled", "focus-within", + "active", "link", "visited" }; static bool isPseudoClassState( const std::string& pseudoClass ) { for ( Uint32 i = 0; i < eeARRAY_SIZE( StatePseudoClasses ); i++ ) { @@ -54,7 +55,11 @@ StyleSheetSelectorRule::toPseudoClass( std::string_view cls ) { return StyleSheetSelectorRule::PseudoClasses::Disabled; if ( "focus-within" == cls ) return StyleSheetSelectorRule::PseudoClasses::FocusWithin; - eeASSERT( false ); + if ( "link" == cls ) + return StyleSheetSelectorRule::PseudoClasses::Link; + if ( "visited" == cls ) + return StyleSheetSelectorRule::PseudoClasses::Visited; + // eeASSERT( false ); return StyleSheetSelectorRule::PseudoClasses::None; } diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 09a9b3b06..f778b1767 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -24,6 +24,10 @@ PropertyDefinition& StyleSheetSpecification::registerProperty( const std::string return mPropertySpecification->registerProperty( propertyVame, defaultValue, inherited ); } +const PropertyDefinition* StyleSheetSpecification::getProperty( const PropertyId& id ) const { + return mPropertySpecification->getProperty( id ); +} + const PropertyDefinition* StyleSheetSpecification::getProperty( const Uint32& id ) const { return mPropertySpecification->getProperty( id ); } @@ -412,6 +416,14 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "focusable", "true" ).setType( PropertyType::Bool ); registerProperty( "expand-text", "false" ).setType( PropertyType::Bool ); registerProperty( "colspan", "1" ).setType( PropertyType::NumberInt ); + registerProperty( "table-layout", "auto" ).setType( PropertyType::String ); + registerProperty( "cellpadding", "0" ).setType( PropertyType::NumberLength ); + registerProperty( "cellspacing", "0" ).setType( PropertyType::NumberLength ); + registerProperty( "size", "20" ).setType( PropertyType::NumberInt ); + registerProperty( "type", "text" ).setType( PropertyType::String ); + registerProperty( "rows", "2" ).setType( PropertyType::NumberInt ); + registerProperty( "cols", "20" ).setType( PropertyType::NumberInt ); + registerProperty( "input-mode", "normal" ).setType( PropertyType::String ); registerProperty( "inner-widget-orientation", "widgeticontextbox" ) .setType( PropertyType::String ); @@ -731,23 +743,31 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { String::removeExtraSpaces( value ); if ( value.empty() ) return {}; + std::vector properties; const std::vector propNames( shorthand->getProperties() ); + if ( propNames.size() != 4 ) { - Log::error( "ShorthandType::Box properties must be 4 for %s", - shorthand->getName().c_str() ); + Log::error( "ShorthandType::Box properties must be 4 for %s", shorthand->getName() ); return properties; } auto ltrbSplit = String::split( value, ' ', true ); + if ( ltrbSplit.empty() ) + return properties; + + // Apply CSS shorthand rules (Top, Right, Bottom, Left) + std::string top = ltrbSplit[0]; + std::string right = ltrbSplit.size() > 1 ? ltrbSplit[1] : top; + std::string bottom = ltrbSplit.size() > 2 ? ltrbSplit[2] : top; + std::string left = ltrbSplit.size() > 3 ? ltrbSplit[3] : right; + + // propNames order is Top, Right, Bottom, Left + properties.emplace_back( StyleSheetProperty( propNames[0], top ) ); + properties.emplace_back( StyleSheetProperty( propNames[1], right ) ); + properties.emplace_back( StyleSheetProperty( propNames[2], bottom ) ); + properties.emplace_back( StyleSheetProperty( propNames[3], left ) ); - if ( ltrbSplit.size() >= 2 ) { - for ( size_t i = 0; i < ltrbSplit.size(); i++ ) - properties.emplace_back( StyleSheetProperty( propNames[i], ltrbSplit[i] ) ); - } else if ( ltrbSplit.size() == 1 ) { - for ( size_t i = 0; i < propNames.size(); i++ ) - properties.emplace_back( StyleSheetProperty( propNames[i], ltrbSplit[0] ) ); - } return properties; }; @@ -961,7 +981,7 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { String::isNumber( tok[0] ) || tok[0] == '-' || tok[0] == '.' || tok[0] == '+' ) { positionStr += tok + " "; - } else if ( Color::isColorString( tok ) ) { + } else { int pos = getIndexEndingWith( propNames, "-color" ); if ( pos != -1 ) properties.emplace_back( StyleSheetProperty( propNames[pos], value ) ); @@ -1031,7 +1051,7 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { mShorthandParsers["border-side"] = []( const ShorthandDefinition* shorthand, std::string value ) -> std::vector { value = String::trim( value ); - if ( value.empty() || "none" == value ) + if ( value.empty() ) return {}; std::vector properties; @@ -1042,8 +1062,16 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { if ( -1 != String::valueIndex( tok, "none;hidden;dotted;dashed;solid;double;groove;ridge;inset;outset" ) ) { - int pos = getIndexEndingWith( propNames, "-style" ); + + // At least reset the border width if "none" was used + if ( "none" == tok ) { + int pos = getIndexEndingWith( propNames, "-width" ); + if ( pos != -1 ) + properties.emplace_back( StyleSheetProperty( propNames[pos], "0" ) ); + } + // boder-style is not implemented yet + int pos = getIndexEndingWith( propNames, "-style" ); if ( pos != -1 ) continue; } else if ( Color::isColorString( tok ) || String::startsWith( tok, "var(" ) ) { diff --git a/src/eepp/ui/doc/foldrangeservice.cpp b/src/eepp/ui/doc/foldrangeservice.cpp index 34d1bb38d..fea612b90 100644 --- a/src/eepp/ui/doc/foldrangeservice.cpp +++ b/src/eepp/ui/doc/foldrangeservice.cpp @@ -3,10 +3,81 @@ #include #include +#include + #include namespace EE { namespace UI { namespace Doc { +static void walkGumboASTForFolding( GumboNode* node, std::vector& regions ) { + if ( node->type != GUMBO_NODE_ELEMENT && node->type != GUMBO_NODE_DOCUMENT ) { + return; + } + + if ( node->type == GUMBO_NODE_ELEMENT ) { + // 1. Check if both the opening and closing tags physically exist in the source text. + // Gumbo will sometimes synthesize missing tags (like or ) to fix bad HTML. + // We only want to fold tags the user actually typed. + if ( node->v.element.original_tag.length > 0 && + node->v.element.original_end_tag.length > 0 ) { + + // 2. Gumbo's source positions are 1-indexed. eepp TextPositions are 0-indexed. + Int64 startLine = static_cast( node->v.element.start_pos.line ) - 1; + Int64 endLine = static_cast( node->v.element.end_pos.line ) - 1; + + // 3. Only create a fold region if the tag spans multiple lines. + if ( endLine > startLine ) { + // We create a range starting from the `<` of the opening tag + // to the `<` of the closing tag. + Int64 startCol = static_cast( node->v.element.start_pos.column ) - 1; + Int64 endCol = static_cast( node->v.element.end_pos.column ) - 1; + + regions.emplace_back( TextPosition( startLine, startCol ), + TextPosition( endLine, endCol ) ); + } + } + + // 4. Recursively walk children + GumboVector* children = &node->v.element.children; + for ( unsigned int i = 0; i < children->length; ++i ) { + walkGumboASTForFolding( static_cast( children->data[i] ), regions ); + } + } else if ( node->type == GUMBO_NODE_DOCUMENT ) { + // Root document node, just process children + GumboVector* children = &node->v.document.children; + for ( unsigned int i = 0; i < children->length; ++i ) { + walkGumboASTForFolding( static_cast( children->data[i] ), regions ); + } + } +} + +static std::vector findFoldingRangesTag( TextDocument* doc ) { + Clock c; + std::vector regions; + + if ( doc->linesCount() <= 2 ) + return regions; + + // Extract the full document text as a UTF-8 string for Gumbo + std::string fullText = doc->toUtf8String(); + Log::debug( "findFoldingRangesTag for \"%s\" doc to string took: %s", doc->getFilePath(), + c.getElapsedTime().toString() ); + + // Parse the text + GumboOutput* output = gumbo_parse( fullText.c_str() ); + + // Walk the AST and populate the folding regions + walkGumboASTForFolding( output->root, regions ); + + // Clean up + gumbo_destroy_output( &kGumboDefaultOptions, output ); + + Log::debug( "findFoldingRangesTag for \"%s\" took %s", doc->getFilePath(), + c.getElapsedTime().toString() ); + + return regions; +} + static std::vector findFoldingRangesBraces( TextDocument* doc ) { Clock c; std::vector regions; @@ -117,8 +188,7 @@ static std::vector findFoldingRangesMarkdown( TextDocument* doc ) { for ( size_t lineIdx = 0; lineIdx < lineCount; lineIdx++ ) { const String& lineText = doc->line( lineIdx ).getText(); - String::View trimmed = - String::trim( lineText.view() ); + String::View trimmed = String::trim( lineText.view() ); if ( inCodeBlock ) { if ( String::startsWith( trimmed, "```" ) ) { @@ -204,9 +274,7 @@ bool FoldRangeService::canFold() const { return false; if ( mProvider && mProvider->foldingRangeProvider() ) return true; - auto type = mDoc->getSyntaxDefinition().getFoldRangeType(); - return type == FoldRangeType::Braces || type == FoldRangeType::Indentation || - type == FoldRangeType::Markdown; + return mDoc->getSyntaxDefinition().getFoldRangeType() != FoldRangeType::Undefined; } void FoldRangeService::findRegions() { @@ -231,9 +299,13 @@ void FoldRangeService::findRegionsNative() { break; case FoldRangeType::Indentation: setFoldingRegions( findFoldingRangesIndentation( mDoc ) ); + break; case FoldRangeType::Markdown: setFoldingRegions( findFoldingRangesMarkdown( mDoc ) ); + break; case FoldRangeType::Tag: + setFoldingRegions( findFoldingRangesTag( mDoc ) ); + break; case FoldRangeType::Undefined: break; } diff --git a/src/eepp/ui/doc/languages/configfile.cpp b/src/eepp/ui/doc/languages/configfile.cpp index b32288187..d1ecf68fe 100644 --- a/src/eepp/ui/doc/languages/configfile.cpp +++ b/src/eepp/ui/doc/languages/configfile.cpp @@ -11,16 +11,16 @@ void addConfigFile() { { "%.ini$", "%.conf$", "%.desktop$", "%.service$", "%.cfg$", "%.properties$", "%.wrap$", "%.dev$", "Doxyfile", "%.timer$", "%.rules$" }, { - { { "%s*#%x%x%x%x%x%x%x%x" }, "string" }, - { { "%s*#%x%x%x%x%x%x" }, "string" }, + { { "%f[^%s=,]#%x%x%x%x%x%x%x%x" }, "string" }, + { { "%f[^%s=,]#%x%x%x%x%x%x" }, "string" }, { { "^#.-\n" }, "comment" }, { { "^;.-\n" }, "comment" }, - { { "%s#.-\n" }, "comment" }, + { { "%f[^%s]#.-\n" }, "comment" }, { { "[%a_][%w-+_%s%p]-%f[=]" }, "keyword" }, { { "\"", "\"", "\\" }, "string" }, { { "'", "'", "\\" }, "string" }, { { "^%[.-%]" }, "type" }, - { { "%s%[.-%]" }, "type" }, + { { "%f[^%s]%[.-%]" }, "type" }, { { "^%s*(%w[%w%d_-]+)%s?%f[{]" }, "keyword" }, { { "[={}]" }, "operator" }, { { "https?://[%w_.~!*:@&+$/?%%#-]-%w[-.%w]*%.%w%w%w?%w?:?%d*/?[%w_.~!*:@&+$/" diff --git a/src/eepp/ui/doc/languages/css.cpp b/src/eepp/ui/doc/languages/css.cpp index c98e26a4a..b6a62d88c 100644 --- a/src/eepp/ui/doc/languages/css.cpp +++ b/src/eepp/ui/doc/languages/css.cpp @@ -188,7 +188,6 @@ void addCSS() { { "rosybrown", "literal" }, { "viewpager", "keyword" }, { "tab", "keyword" }, - { "inputpassword", "keyword" }, { "window", "keyword" }, { "tooltip", "keyword" }, { "scrollview", "keyword" }, @@ -261,7 +260,6 @@ void addCSS() { { "true", "literal" }, { "sizens", "literal" }, { "magenta", "literal" }, - { "textinputpassword", "keyword" }, { "important", "literal" }, { "a", "keyword" }, { "abbr", "keyword" }, @@ -399,7 +397,6 @@ void addCSS() { { "TabWidget", "keyword" }, { "TextEdit", "keyword" }, { "TextInput", "keyword" }, - { "TextInputPassword", "keyword" }, { "Loader", "keyword" }, { "SelectButton", "keyword" }, { "Window", "keyword" }, @@ -412,6 +409,7 @@ void addCSS() { { "CodeEditor", "keyword" }, { "Splitter", "keyword" }, { "TreeView", "keyword" }, + { "TextArea", "keyword" }, { "TableView", "keyword" }, { "ListView", "keyword" }, { "StackWidget", "keyword" }, @@ -427,6 +425,7 @@ void addCSS() { { "ImageViewer", "keyword" }, { "AudioPlayer", "keyword" }, { "Table", "keyword" }, + { "MarkdownView", "keyword" }, }, "", diff --git a/src/eepp/ui/doc/languages/html.cpp b/src/eepp/ui/doc/languages/html.cpp index d4d6396b2..4689d7c36 100644 --- a/src/eepp/ui/doc/languages/html.cpp +++ b/src/eepp/ui/doc/languages/html.cpp @@ -9,7 +9,7 @@ void addHTML() { ->add( { "HTML", - { "%.[mp]?html?$", "%.handlebars$" }, + { "%.[mpx]?html?$", "%.handlebars$" }, { { { "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*['\"]%a+/" "[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", @@ -50,7 +50,8 @@ void addHTML() { } ) .setAutoCloseXMLTags( true ) - .setBlockComment( { "" } ); + .setBlockComment( { "" } ) + .setFoldRangeType( FoldRangeType::Tag ); } }}}} // namespace EE::UI::Doc::Language diff --git a/src/eepp/ui/doc/languages/xml.cpp b/src/eepp/ui/doc/languages/xml.cpp index 31df8c520..e8021061e 100644 --- a/src/eepp/ui/doc/languages/xml.cpp +++ b/src/eepp/ui/doc/languages/xml.cpp @@ -37,7 +37,8 @@ void addXML() { } ) .setAutoCloseXMLTags( true ) - .setBlockComment( { "" } ); + .setBlockComment( { "" } ) + .setFoldRangeType( FoldRangeType::Tag ); } }}}} // namespace EE::UI::Doc::Language diff --git a/src/eepp/ui/doc/syntaxcolorscheme.cpp b/src/eepp/ui/doc/syntaxcolorscheme.cpp index 50bbb7448..2fb94daed 100644 --- a/src/eepp/ui/doc/syntaxcolorscheme.cpp +++ b/src/eepp/ui/doc/syntaxcolorscheme.cpp @@ -112,7 +112,7 @@ SyntaxColorScheme::Style parseStyle( style.style |= Text::Italic; else if ( "underline" == val || "underlined" == val ) style.style |= Text::Underlined; - else if ( "strikethrough" == val ) + else if ( "strikethrough" == val || "line-through" == val ) style.style |= Text::StrikeThrough; else if ( "shadow" == val ) style.style |= Text::Shadow; diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index e52778a8b..a6649d8ee 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -16,6 +16,8 @@ #include #include +#include + using namespace std::literals; using namespace EE::Network; @@ -29,8 +31,6 @@ static constexpr char DEFAULT_NON_WORD_CHARS[] = " \t\n/\\()\"':,.;<>~!@#$%^&*|+ static UnorderedSet TEXT_DOCUMENT_COMMANDS = {}; -#include // Ensure this is included for std::string_view - bool TextDocument::fileMightBeBinary( const std::string& file ) { static constexpr size_t MAX_READ = 4096; static constexpr std::array NULL_SEQUENCE = { 0, 0, 0, 0 }; @@ -662,14 +662,14 @@ void TextDocument::guessIndentType() { int guessCountdown = 10; for ( size_t i = start; i < end; i++ ) { const String& text = mLines[i].getText(); - std::string match = - LuaPattern::match( text.size() > 128 ? text.substr( 0, 12 ) : text, "^ +" ); + std::string match = LuaPattern::match( + text.size() > 128 ? text.substr( 0, 128 ).toUtf8() : text.toUtf8(), "^ +" ); if ( !match.empty() ) { guessSpaces++; guessWidth[match.size()]++; guessCountdown--; } else { - match = LuaPattern::match( mLines[i].getText(), "^\t+" ); + match = LuaPattern::match( mLines[i].getText().toUtf8(), "^\t+" ); if ( !match.empty() ) { guessTabs++; guessCountdown--; @@ -1043,8 +1043,9 @@ bool TextDocument::save( IOStream& stream, bool keepUndoRedoStatus ) { } size_t lastLine = linesCount() - 1; + std::string text; for ( size_t i = 0; i <= lastLine; i++ ) { - std::string text( getLineTextUtf8( i ) ); + text = getLineTextUtf8( i ); if ( !keepUndoRedoStatus && mTrimTrailingWhitespaces && text.size() > 1 && whitespaces.find( text[text.size() - 2] ) != std::string::npos ) { @@ -1287,6 +1288,15 @@ TextRange TextDocument::addSelection( TextRange selection ) { return selection; } +int TextDocument::selectionIndex( TextRange selection ) const { + if ( mSelection.exists( selection ) ) + return mSelection.findIndex( selection ); + selection = sanitizeRange( selection ); + if ( mSelection.exists( selection ) ) + return mSelection.findIndex( selection ); + return -1; +} + void TextDocument::popSelection() { mSelection.pop_back(); if ( mLastSelection >= mSelection.size() ) @@ -1357,18 +1367,47 @@ std::string TextDocument::getHashHexString() const { } String TextDocument::getText( const TextRange& range ) const { - TextRange nrange = sanitizeRange( range.normalized() ); Lock l( mLinesMutex ); - if ( nrange.start().line() == nrange.end().line() ) { - return mLines[nrange.start().line()].substr( - nrange.start().column(), nrange.end().column() - nrange.start().column() ); + Lock l2( *mDocumentMutex ); + + TextRange nrange = sanitizeRange( range.normalized() ); + if ( !nrange.hasSelection() ) + return String(); + + Int64 startLine = nrange.start().line(); + Int64 endLine = nrange.end().line(); + Int64 startCol = nrange.start().column(); + Int64 endCol = nrange.end().column(); + + std::size_t totalSize = 0; + if ( startLine == endLine ) { + totalSize = endCol - startCol; + } else { + totalSize += ( mLines[startLine].size() - startCol ); + for ( Int64 i = startLine + 1; i < endLine; ++i ) { + totalSize += mLines[i].size(); + } + totalSize += endCol; } - std::vector lines = { mLines[nrange.start().line()].substr( nrange.start().column() ) }; - for ( auto i = nrange.start().line() + 1; i <= nrange.end().line() - 1; i++ ) { - lines.emplace_back( mLines[i].getText() ); + + String result; + result.reserve( totalSize ); + + if ( startLine == endLine ) { + result.append( mLines[startLine].getText(), startCol, endCol - startCol ); + } else { + result.append( mLines[startLine].getText(), startCol, mLines[startLine].size() - startCol ); + + for ( Int64 i = startLine + 1; i < endLine; ++i ) { + result.append( mLines[i].getText() ); + } + + if ( endCol > 0 ) { + result.append( mLines[endLine].getText(), 0, endCol ); + } } - lines.emplace_back( mLines[nrange.end().line()].substr( 0, nrange.end().column() ) ); - return String::join( lines, -1 ); + + return result; } String TextDocument::getText() const { @@ -1388,6 +1427,38 @@ String TextDocument::getAllSelectedText() const { return text; } +String TextDocument::toString() { + Lock l( mLinesMutex ); + Lock l2( *mDocumentMutex ); + String stream; + std::size_t totalSize = 0; + for ( const auto& line : mLines ) + totalSize += line.size(); + stream.reserve( totalSize ); + for ( const auto& line : mLines ) + stream.append( line.getText() ); + return stream; +} + +std::string TextDocument::toUtf8String() { + Lock l( mLinesMutex ); + Lock l2( *mDocumentMutex ); + std::string stream; + std::size_t totalCodepoints = 0; + for ( const auto& line : mLines ) + totalCodepoints += line.size(); + + // Heuristic reserve: Codepoints + 25% to account for UTF-8 expansion + stream.reserve( totalCodepoints + ( totalCodepoints >> 2 ) ); + + for ( const auto& line : mLines ) { + const String& text = line.getText(); + // Low-level conversion directly into the stream buffer + Utf32::toUtf8( text.begin(), text.end(), std::back_inserter( stream ) ); + } + return stream; +} + std::vector TextDocument::getCommandList() const { std::vector cmds; cmds.reserve( mCommands.size() + mRefCommands.size() ); @@ -2143,27 +2214,67 @@ std::vector TextDocument::autoCloseBrackets( const String& text ) { continue; } - if ( isClose && !isSame ) + if ( isClose && !isSame ) { + mustClose = false; + } else if ( !isClose && !isNonWord( ch ) ) { + mustClose = false; + } + } + + if ( mustClose && isSame ) { + Int64 left = sel.start().column() - 1; + Int64 right = sel.start().column(); + const String& lineText = line( sel.start().line() ).getText(); + Int64 len = lineText.size(); + Int64 limitLeft = eemax( 0ll, sel.start().column() - 512 ); + Int64 limitRight = eemin( len, sel.start().column() + 512 ); + int unclosedQuotes = 0; + while ( left >= limitLeft || right < limitRight ) { + bool matchLeft = left >= limitLeft && lineText[left] == text[0]; + bool matchRight = right < limitRight && lineText[right] == text[0]; + if ( matchLeft && matchRight ) { + left--; + right++; + } else if ( matchLeft ) { + unclosedQuotes++; + left--; + } else if ( matchRight ) { + unclosedQuotes++; + right++; + } else { + if ( left >= limitLeft ) + left--; + if ( right < limitRight ) + right++; + } + } + if ( unclosedQuotes % 2 != 0 ) + mustClose = false; + } + + if ( mustClose && !isSame && !isClose ) { + int balance = 0; + int unmatchedRight = 0; + const String& lineText = line( sel.start().line() ).getText(); + Int64 len = lineText.size(); + Int64 limitLeft = eemax( 0, sel.start().column() - 512 ); + Int64 limitRight = eemin( len, sel.start().column() + 512 ); + for ( Int64 k = limitLeft; k < limitRight; ++k ) { + if ( lineText[k] == text[0] ) { + balance++; + } else if ( lineText[k] == closeChar ) { + if ( balance > 0 ) { + balance--; + } else if ( k >= sel.start().column() ) { + unmatchedRight++; + } + } + } + if ( unmatchedRight > 0 ) mustClose = false; } if ( mustClose ) { - /* // I'm not entirely convinced about this - TextPosition openStart = positionOffset( sel.start(), 1 ); - if ( openStart != sel.start() ) { - int maxIt = 100; - while ( maxIt-- > 0 && openStart < endOfDoc() && - isSpace( getChar( openStart ) ) ) { - openStart = nextChar( openStart ); - } - if ( openStart < endOfDoc() && maxIt > 0 && - getChar( openStart ) == closeChar ) { - inserted.push_back( false ); - continue; - } - } - */ - setSelection( i, positionOffset( insert( i, sel.start(), text + String( closeChar ) ), -1 ) ); inserted.push_back( true ); @@ -2645,21 +2756,65 @@ void TextDocument::selectAll() { } void TextDocument::newLine() { - String input( "\n" ); - TextPosition start = getSelection().start(); - TextPosition indent = startOfContent( getSelection().start() ); - if ( indent.column() != 0 ) - input.append( line( start.line() ).getText().substr( 0, indent.column() ) ); - textInput( input ); + BoolScopedOp op( mDoingTextInput, true ); + BoolScopedOp op2( mInsertingText, true ); + AtomicBoolScopedOp op3( mRunningTransaction, true ); + mUndoStack.clearRedoStack(); + Time time = mTimer.getElapsedTime(); + + for ( int i = (int)mSelection.size() - 1; i >= 0; --i ) { + TextPosition start = getSelectionIndex( i ).start(); + String indentStr; + if ( mAutoIndent != AutoIndentConfig::None ) { + TextPosition indentPos = startOfContent( start ); + if ( indentPos.column() != 0 ) + indentStr = line( start.line() ).getText().substr( 0, indentPos.column() ); + } + + String input( "\n" ); + input.append( indentStr ); + + bool isPair = false; + if ( mAutoIndent == AutoIndentConfig::Smart && start > startOfDoc() && + start < endOfDoc() ) { + String::StringBaseType curChar = getChar( start ); + String::StringBaseType prevChar = getPrevChar( start ); + for ( const auto& pair : mAutoCloseBracketsPairs ) { + if ( prevChar == pair.first && curChar == pair.second && + pair.first != pair.second ) { + isPair = true; + break; + } + } + } + + if ( mSelection[i].hasSelection() ) + deleteTo( i, 0 ); + + if ( isPair ) { + String extraIndent = input + getIndentString(); + String closingLine = "\n" + indentStr; + + insert( i, getSelectionIndex( i ).start(), extraIndent + closingLine, + mUndoStack.getUndoStackContainer(), time ); + setSelection( i, positionOffset( getSelectionIndex( i ).start(), extraIndent.size() ) ); + } else { + setSelection( i, insert( i, getSelectionIndex( i ).start(), input, + mUndoStack.getUndoStackContainer(), time ) ); + } + } + mLastCursorChangeWasInteresting = true; } void TextDocument::newLineAbove() { for ( size_t i = 0; i < mSelection.size(); ++i ) { String input( "\n" ); TextPosition start = getSelectionIndex( i ).start(); - TextPosition indent = startOfContent( getSelectionIndex( i ).start() ); - if ( indent.column() != 0 ) - input.insert( 0, line( start.line() ).getText().substr( 0, indent.column() ) ); + if ( mAutoIndent != AutoIndentConfig::None ) { + TextPosition indent = startOfContent( getSelectionIndex( i ).start() ); + if ( indent.column() != 0 ) + input.insert( 0, line( start.line() ).getText().substr( 0, indent.column() ) ); + } insert( i, { start.line(), 0 }, input ); setSelection( i, { start.line(), (Int64)input.size() } ); } @@ -2899,6 +3054,14 @@ void TextDocument::setIndentType( const IndentType& indentType ) { mIndentType = indentType; } +const TextDocument::AutoIndentConfig& TextDocument::getAutoIndent() const { + return mAutoIndent; +} + +void TextDocument::setAutoIndent( const AutoIndentConfig& autoIndent ) { + mAutoIndent = autoIndent; +} + void TextDocument::undo() { setRunningTransaction( true ); bool stackWasFull = mUndoStack.getMaxStackSize() == mUndoStack.getUndoStackContainer().size(); @@ -4614,6 +4777,35 @@ void TextDocument::convertIndentationToSpaces() { } } +void TextDocument::clearIndentation() { + TextRanges oldSelections = mSelection; + std::set linesToClear; + + if ( !hasSelection() ) { + linesToClear.insert( getSelection().start().line() ); + } else { + for ( const auto& sel : mSelection ) { + TextRange normalizedSel = sel.normalized(); + for ( Int64 lineNum = normalizedSel.start().line(); + lineNum <= normalizedSel.end().line(); ++lineNum ) { + linesToClear.insert( lineNum ); + } + } + } + + for ( Int64 lineNum : linesToClear ) { + TextRange lineRange = getLineRange( lineNum ); + replace( "^%s+", "", lineRange.start(), true, false, FindReplaceType::LuaPattern, + lineRange ); + } + + if ( !linesToClear.empty() ) { + setSelection( oldSelections ); + notifySelectionChanged(); + notifyCursorChanged(); + } +} + void TextDocument::initializeCommands() { mCommands["reset-document"] = [this] { reset(); }; mCommands["save-doc"] = [this] { save(); }; @@ -4687,6 +4879,7 @@ void TextDocument::initializeCommands() { mCommands["duplicate-line-or-selection"] = [this] { duplicateLineOrSelection(); }; mCommands["convert-indentation-to-tabs"] = [this] { convertIndentationToTabs(); }; mCommands["convert-indentation-to-spaces"] = [this] { convertIndentationToSpaces(); }; + mCommands["clear-indentation"] = [this] { clearIndentation(); }; if ( TEXT_DOCUMENT_COMMANDS.empty() ) { for ( const auto& [cmd, _] : mCommands ) diff --git a/src/eepp/ui/doc/textrange.cpp b/src/eepp/ui/doc/textrange.cpp index 486c678e7..48aa95c5d 100644 --- a/src/eepp/ui/doc/textrange.cpp +++ b/src/eepp/ui/doc/textrange.cpp @@ -164,6 +164,75 @@ TextRange TextRange::convertToLineColumn( const std::string_view& text, Int64 st return convertToLineColumn( text, startOffset, endOffset ); } +template +TextSelectionRange TextRange::convertToOffset( const StringType& text, const TextRange& range ) { + if ( !range.isValid() ) + return { -1, -1 }; + + Int64 startOffset = -1; + Int64 endOffset = -1; + Int64 currentLine = 0; + Int64 currentCol = 0; + Int64 currentPos = 0; + size_t len = text.length(); + + const TextPosition& start = range.start(); + const TextPosition& end = range.end(); + + for ( size_t i = 0; i <= len; i++ ) { + // Exact match for line and column + if ( startOffset == -1 && currentLine == start.line() && currentCol == start.column() ) { + startOffset = currentPos; + } + + if ( endOffset == -1 && currentLine == end.line() && currentCol == end.column() ) { + endOffset = currentPos; + } + + if ( startOffset != -1 && endOffset != -1 ) + break; + + if ( i == len ) + break; + + if ( text[i] == '\n' ) { + // If the requested column is virtually out of bounds for this line, clamp to the line's + // end (the newline character's pos) + if ( startOffset == -1 && currentLine == start.line() && start.column() > currentCol ) { + startOffset = currentPos; + } + if ( endOffset == -1 && currentLine == end.line() && end.column() > currentCol ) { + endOffset = currentPos; + } + + currentLine++; + currentCol = 0; + } else { + currentCol++; + } + + currentPos++; + } + + // If the range requested lines beyond the text entirely, clamp to the very end of the text + if ( startOffset == -1 ) + startOffset = currentPos; + + if ( endOffset == -1 ) + endOffset = currentPos; + + return { startOffset, endOffset }; +} + +TextSelectionRange TextRange::convertToOffset( const String::View& text, const TextRange& range ) { + return convertToOffset( text, range ); +} + +TextSelectionRange TextRange::convertToOffset( const std::string_view& text, + const TextRange& range ) { + return convertToOffset( text, range ); +} + Int64 TextRange::minimumDistance( const TextRange& other ) const { if ( intersects( other ) ) return 0; diff --git a/src/eepp/ui/htmlinput.cpp b/src/eepp/ui/htmlinput.cpp new file mode 100644 index 000000000..734e2f1ea --- /dev/null +++ b/src/eepp/ui/htmlinput.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { + +HTMLInput* HTMLInput::New() { + return eeNew( HTMLInput, () ); +} + +HTMLInput::HTMLInput() : UIWidget( "input" ) { + mFlags |= UI_HTML_ELEMENT; + mWidthPolicy = SizePolicy::WrapContent; + mHeightPolicy = SizePolicy::WrapContent; + createChildWidget(); +} + +Uint32 HTMLInput::getType() const { + return UI_TYPE_HTML_INPUT; +} + +bool HTMLInput::isType( const Uint32& type ) const { + return HTMLInput::getType() == type || UIWidget::isType( type ); +} + +bool HTMLInput::applyProperty( const StyleSheetProperty& attribute ) { + if ( !attribute.getPropertyDefinition() ) + return false; + + PropertyId id = attribute.getPropertyDefinition()->getPropertyId(); + + switch ( id ) { + case PropertyId::Type: + setInputType( attribute.value() ); + return true; + default: + break; + } + + if ( id != PropertyId::Id && id != PropertyId::Class && id != PropertyId::Type ) { + mProperties[id] = attribute; + if ( mChildWidget ) { + mChildWidget->applyProperty( attribute ); + } + } + + return UIWidget::applyProperty( attribute ); +} + +std::string HTMLInput::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( !propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::Type: + return mInputType; + default: + break; + } + + if ( mChildWidget ) { + std::string val = mChildWidget->getPropertyString( propertyDef, propertyIndex ); + if ( !val.empty() ) + return val; + } + + return UIWidget::getPropertyString( propertyDef, propertyIndex ); +} + +std::vector HTMLInput::getPropertiesImplemented() const { + auto props = UIWidget::getPropertiesImplemented(); + props.push_back( PropertyId::Type ); + return props; +} + +Float HTMLInput::getMinIntrinsicWidth() const { + return mChildWidget ? mChildWidget->getMinIntrinsicWidth() : 0; +} + +Float HTMLInput::getMaxIntrinsicWidth() const { + return mChildWidget ? mChildWidget->getMaxIntrinsicWidth() : 0; +} + +const std::string& HTMLInput::getInputType() const { + return mInputType; +} + +void HTMLInput::setInputType( const std::string& type ) { + if ( mInputType != type ) { + mInputType = type; + createChildWidget(); + } +} + +UIWidget* HTMLInput::getChildWidget() const { + return mChildWidget; +} + +void HTMLInput::createChildWidget() { + if ( mChildWidget ) { + mChildWidget->close(); + mChildWidget = nullptr; + } + + if ( mInputType == "button" || mInputType == "submit" ) { + mChildWidget = UIPushButton::New(); + } else if ( mInputType == "checkbox" ) { + mChildWidget = UICheckBox::New(); + } else if ( mInputType == "hidden" ) { + mChildWidget = UIWidget::New(); + mChildWidget->setVisible( false ); + } else if ( mInputType == "number" ) { + mChildWidget = UISpinBox::New(); + } else if ( mInputType == "password" ) { + mChildWidget = HTMLTextInput::New()->setMode( UITextInput::TextInputMode::Password ); + } else if ( mInputType == "radio" ) { + mChildWidget = UIRadioButton::New(); + } else { + mChildWidget = HTMLTextInput::New(); + } + + mChildWidget->setFlags( UI_HTML_ELEMENT ); + + if ( mChildWidget ) { + mChildWidget->setParent( this ); + mChildWidget->setLayoutWidthPolicy( SizePolicy::WrapContent ); + mChildWidget->setLayoutHeightPolicy( SizePolicy::WrapContent ); + mChildWidget->on( Event::OnSizeChange, + [this]( auto ) { setPixelsSize( mChildWidget->getPixelsSize() ); } ); + for ( const auto& propIt : mProperties ) { + mChildWidget->applyProperty( propIt.second ); + } + } +} + +void HTMLInput::onSizeChange() { + UIWidget::onSizeChange(); +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/htmltextarea.cpp b/src/eepp/ui/htmltextarea.cpp new file mode 100644 index 000000000..5d8df8f17 --- /dev/null +++ b/src/eepp/ui/htmltextarea.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include + +namespace EE { namespace UI { + +HTMLTextArea* HTMLTextArea::New() { + return eeNew( HTMLTextArea, () ); +} + +HTMLTextArea::HTMLTextArea() : UITextEdit() { + setElementTag( "textarea" ); + mFlags |= UI_HTML_ELEMENT; + mWidthPolicy = SizePolicy::WrapContent; + mHeightPolicy = SizePolicy::WrapContent; + invalidateIntrinsicSize(); +} + +Uint32 HTMLTextArea::getType() const { + return UI_TYPE_HTML_TEXTAREA; +} + +bool HTMLTextArea::isType( const Uint32& type ) const { + return HTMLTextArea::getType() == type || UITextEdit::isType( type ); +} + +bool HTMLTextArea::applyProperty( const StyleSheetProperty& attribute ) { + if ( !attribute.getPropertyDefinition() ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::Rows: + setRows( attribute.asUint( 2 ) ); + return true; + case PropertyId::Cols: + setCols( attribute.asUint( 20 ) ); + return true; + default: + break; + } + + return UITextEdit::applyProperty( attribute ); +} + +std::string HTMLTextArea::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( !propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::Rows: + return String::format( "%u", mRows ); + case PropertyId::Cols: + return String::format( "%u", mCols ); + default: + break; + } + + return UITextEdit::getPropertyString( propertyDef, propertyIndex ); +} + +std::vector HTMLTextArea::getPropertiesImplemented() const { + auto props = UITextEdit::getPropertiesImplemented(); + props.push_back( PropertyId::Rows ); + props.push_back( PropertyId::Cols ); + return props; +} + +Float HTMLTextArea::getMinIntrinsicWidth() const { + if ( mCols > 0 && getFont() ) { + Float advance = getFont()->getGlyph( 'M', getFontSize(), false, false ).advance; + Float sbWidth = getVScrollBar() ? getVScrollBar()->getPixelsSize().getWidth() : 0; + return mCols * advance + mPaddingPx.Left + mPaddingPx.Right + sbWidth; + } + return UITextEdit::getMinIntrinsicWidth(); +} + +Float HTMLTextArea::getMaxIntrinsicWidth() const { + return getMinIntrinsicWidth(); +} + +Float HTMLTextArea::getMinIntrinsicHeight() const { + if ( mRows > 0 && getFont() ) { + return mRows * getFont()->getFontHeight( getFontSize() ) + mPaddingPx.Top + + mPaddingPx.Bottom; + } + return 0; +} + +Float HTMLTextArea::getMaxIntrinsicHeight() const { + return getMinIntrinsicHeight(); +} + +void HTMLTextArea::onAutoSize() { + if ( mPacking ) + return; + mPacking = true; + + if ( mWidthPolicy == SizePolicy::WrapContent && getFont() ) { + Float width = getMinIntrinsicWidth(); + if ( width > 0 ) { + setInternalPixelsWidth( width ); + } + } + if ( mHeightPolicy == SizePolicy::WrapContent && getFont() ) { + Float height = getMinIntrinsicHeight(); + if ( height > 0 ) { + setInternalPixelsHeight( height ); + } + } + UITextEdit::onAutoSize(); + mPacking = false; +} + +Uint32 HTMLTextArea::getRows() const { + return mRows; +} + +void HTMLTextArea::setRows( Uint32 rows ) { + if ( mRows != rows ) { + mRows = rows; + invalidateIntrinsicSize(); + onAutoSize(); + } +} + +Uint32 HTMLTextArea::getCols() const { + return mCols; +} + +void HTMLTextArea::setCols( Uint32 cols ) { + if ( mCols != cols ) { + mCols = cols; + invalidateIntrinsicSize(); + onAutoSize(); + } +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/htmltextinput.cpp b/src/eepp/ui/htmltextinput.cpp new file mode 100644 index 000000000..05a6fd588 --- /dev/null +++ b/src/eepp/ui/htmltextinput.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +namespace EE { namespace UI { + +HTMLTextInput* HTMLTextInput::New() { + return eeNew( HTMLTextInput, () ); +} + +HTMLTextInput::HTMLTextInput() : HTMLTextInput( "textinput" ) {} + +HTMLTextInput::HTMLTextInput( const std::string& tag ) : UITextInput( tag ) { + mHtmlSize = 20; + mWidthPolicy = SizePolicy::WrapContent; + mHeightPolicy = SizePolicy::WrapContent; + invalidateIntrinsicSize(); + onAutoSize(); +} + +Uint32 HTMLTextInput::getType() const { + return UI_TYPE_HTML_TEXTINPUT; +} + +bool HTMLTextInput::isType( const Uint32& type ) const { + return HTMLTextInput::getType() == type || UITextInput::isType( type ); +} + +bool HTMLTextInput::applyProperty( const StyleSheetProperty& attribute ) { + if ( !attribute.getPropertyDefinition() ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::Size: + setHtmlSize( attribute.asUint( 20 ) ); + return true; + default: + break; + } + + return UITextInput::applyProperty( attribute ); +} + +std::string HTMLTextInput::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( !propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::Size: + return String::format( "%u", mHtmlSize ); + default: + break; + } + + return UITextInput::getPropertyString( propertyDef, propertyIndex ); +} + +std::vector HTMLTextInput::getPropertiesImplemented() const { + auto props = UITextInput::getPropertiesImplemented(); + props.push_back( PropertyId::Size ); + return props; +} + +Float HTMLTextInput::getMinIntrinsicWidth() const { + if ( mHtmlSize > 0 && getFont() ) { + Float advance = getFont()->getGlyph( 'M', getFontSize(), false, false ).advance; + return mHtmlSize * advance + mPaddingPx.Left + mPaddingPx.Right; + } + return UITextInput::getMinIntrinsicWidth(); +} + +Float HTMLTextInput::getMaxIntrinsicWidth() const { + return getMinIntrinsicWidth(); +} + +void HTMLTextInput::onAutoSize() { + if ( mPacking ) + return; + mPacking = true; + + if ( mWidthPolicy == SizePolicy::WrapContent && getFont() ) { + Float width = getMinIntrinsicWidth(); + if ( width > 0 ) { + setInternalPixelsWidth( width ); + } + } + + UITextInput::onAutoSize(); + + mPacking = false; +} + +Uint32 HTMLTextInput::getHtmlSize() const { + return mHtmlSize; +} + +void HTMLTextInput::setHtmlSize( Uint32 size ) { + if ( mHtmlSize != size ) { + mHtmlSize = size; + invalidateIntrinsicSize(); + onAutoSize(); + onSizeChange(); + } +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/tools/htmlformatter.cpp b/src/eepp/ui/tools/htmlformatter.cpp index 338b688c2..a8985f131 100644 --- a/src/eepp/ui/tools/htmlformatter.cpp +++ b/src/eepp/ui/tools/htmlformatter.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -5,8 +6,146 @@ #define PUGIXML_HEADER_ONLY #include +#include + +using namespace EE::System; + namespace EE { namespace UI { namespace Tools { +// Helper to escape text so pugixml doesn't crash on <, >, or & +// and scrubs unwanted Unicode characters (like visible Non-Breaking Spaces) +static std::string escapeXML( std::string_view input ) { + std::string out; + out.reserve( input.size() * 1.1 ); + + for ( size_t i = 0; i < input.size(); ++i ) { + unsigned char c = input[i]; + + // --- INTERCEPT UTF-8 NON-BREAKING SPACE --- + // A non-breaking space is encoded in UTF-8 as two bytes: 0xC2 0xA0 + if ( c == 0xC2 && i + 1 < input.size() && (unsigned char)input[i + 1] == 0xA0 ) { + out += ' '; // Inject a standard ASCII space (Dec 32) + i++; // Skip the second byte (0xA0) + continue; + } + + switch ( c ) { + case '<': + out += "<"; + break; + case '>': + out += ">"; + break; + case '&': + out += "&"; + break; + case '"': + out += """; + break; + case '\'': + out += "'"; + break; + default: + out += c; + break; + } + } + return out; +} + +// Recursive function to walk the Gumbo AST and build strict XML +static void serializeGumboNodeToXML( GumboNode* node, std::string& out ) { + if ( !node ) + return; + + switch ( node->type ) { + case GUMBO_NODE_DOCUMENT: { + // Root document, process all children + GumboVector* children = &node->v.document.children; + for ( unsigned int i = 0; i < children->length; ++i ) { + serializeGumboNodeToXML( static_cast( children->data[i] ), out ); + } + break; + } + case GUMBO_NODE_ELEMENT: { + // Handle the HTML tag + std::string tag; + if ( node->v.element.tag != GUMBO_TAG_UNKNOWN ) { + tag = gumbo_normalized_tagname( node->v.element.tag ); + } else { + // For custom tags (like ), Gumbo stores them as unknown. + // We extract the original tag string safely. + GumboStringPiece* original_tag = &node->v.element.original_tag; + gumbo_tag_from_original_text( original_tag ); // standardizes it + if ( original_tag->data && original_tag->length > 0 ) { + // Strip the `<` and any trailing spaces/brackets to get just the name + std::string raw( original_tag->data, original_tag->length ); + size_t start = raw.find_first_not_of( "< " ); + size_t end = raw.find_first_of( " >\r\n\t", start ); + if ( start != std::string::npos ) { + tag = raw.substr( start, end - start ); + } + } + if ( tag.empty() ) + tag = "unknown"; + } + + out += "<" + tag; + + // --- Process Attributes --- + GumboVector* attrs = &node->v.element.attributes; + for ( unsigned int i = 0; i < attrs->length; ++i ) { + GumboAttribute* attr = static_cast( attrs->data[i] ); + std::string attr_name = attr->name; + std::string attr_value = attr->value; + + // BOOLEAN ATTRIBUTE FIX: + // If Gumbo parsed an attribute without a value (e.g., ), + // it often sets the value to empty. We enforce XML strictness. + if ( attr_value.empty() ) { + attr_value = attr_name; + } + + out += " " + attr_name + "=\"" + escapeXML( attr_value ) + "\""; + } + + // --- Handle Void Tags vs Standard Tags --- + // We enforce XML closing rules so pugixml doesn't fail + static const UnorderedSet void_tags = { + "area", "base", "br", "col", "embed", "hr", "img", + "input", "link", "meta", "param", "source", "track", "wbr" }; + + if ( void_tags.count( tag ) ) { + out += " />"; // Self-close + } else { + out += ">"; + + // Recursively process children + GumboVector* children = &node->v.element.children; + for ( unsigned int i = 0; i < children->length; ++i ) { + serializeGumboNodeToXML( static_cast( children->data[i] ), out ); + } + + out += ""; + } + break; + } + case GUMBO_NODE_TEXT: + case GUMBO_NODE_WHITESPACE: + case GUMBO_NODE_CDATA: { + // Safely escape and write all raw text (including the insides of scripts/styles) + if ( node->v.text.text ) { + out += escapeXML( node->v.text.text ); + } + break; + } + case GUMBO_NODE_COMMENT: + case GUMBO_NODE_TEMPLATE: + // We silently ignore comments to prevent XML double-hyphen crashes + break; + } +} + // In HTML, whitespace processing depends heavily on whether elements are block-level // or inline-level. The HTML specification states that sequences of whitespace // (spaces, tabs, newlines) inside inline formatting contexts are collapsed into a @@ -42,7 +181,7 @@ bool HTMLFormatter::isInlineNode( const pugi::xml_node& node ) { String::iequals( name, "em" ) || String::iequals( name, "s" ) || String::iequals( name, "u" ) || String::iequals( name, "br" ) || String::iequals( name, "code" ) || String::iequals( name, "img" ) || - String::iequals( name, "mark" ); + String::iequals( name, "mark" ) || String::iequals( name, "font" ); } // "Significant text" in the context of HTML whitespace collapsing means any text @@ -204,4 +343,21 @@ String HTMLFormatter::collapseXmlWhitespace( const String& text, const pugi::xml return res; } +std::string HTMLFormatter::HTMLtoXML( const std::string& layoutString ) { + if ( layoutString.empty() ) + return ""; + + // 1. Parse the dirty HTML into a Gumbo AST + GumboOutput* output = gumbo_parse( layoutString.c_str() ); + + // 2. Serialize the AST into strict XML + std::string strict_xml; + serializeGumboNodeToXML( output->root, strict_xml ); + + // 3. Cleanup Gumbo's memory + gumbo_destroy_output( &kGumboDefaultOptions, output ); + + return strict_xml; +} + }}} // namespace EE::UI::Tools diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index cbd2996f7..fe4a46ddb 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -18,14 +18,24 @@ const std::map UICodeEditorSplitter::getDefa return localKeybindings; } +#if EE_PLATFORM == EE_PLATFORM_MACOS +static Uint32 DefaultSwitchToTabModifier = KEYMOD_CTRL; +#else +static Uint32 DefaultSwitchToTabModifier = KeyMod::getDefaultModifier(); +#endif + +Uint32 UICodeEditorSplitter::getDefaultSwitchToTabModifier() { + return DefaultSwitchToTabModifier; +} + const std::map UICodeEditorSplitter::getLocalDefaultKeybindings() { return { { { KEY_S, KeyMod::getDefaultModifier() }, "save-doc" }, { { KEY_T, KeyMod::getDefaultModifier() }, "create-new" }, { { KEY_W, KeyMod::getDefaultModifier() }, "close-tab" }, - { { KEY_TAB, KeyMod::getDefaultModifier() }, "next-tab" }, - { { KEY_TAB, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "previous-tab" }, + { { KEY_TAB, DefaultSwitchToTabModifier }, "next-tab" }, + { { KEY_TAB, DefaultSwitchToTabModifier | KEYMOD_SHIFT }, "previous-tab" }, { { KEY_J, KEYMOD_LALT | KEYMOD_SHIFT }, "split-left" }, { { KEY_L, KEYMOD_LALT | KEYMOD_SHIFT }, "split-right" }, { { KEY_I, KEYMOD_LALT | KEYMOD_SHIFT }, "split-top" }, @@ -35,16 +45,16 @@ UICodeEditorSplitter::getLocalDefaultKeybindings() { { { KEY_L, KeyMod::getDefaultModifier() | KEYMOD_LALT }, "switch-to-next-split" }, { { KEY_N, KeyMod::getDefaultModifier() | KEYMOD_LALT }, "switch-to-previous-colorscheme" }, { { KEY_M, KeyMod::getDefaultModifier() | KEYMOD_LALT }, "switch-to-next-colorscheme" }, - { { KEY_1, KeyMod::getDefaultModifier() }, "switch-to-tab-1" }, - { { KEY_2, KeyMod::getDefaultModifier() }, "switch-to-tab-2" }, - { { KEY_3, KeyMod::getDefaultModifier() }, "switch-to-tab-3" }, - { { KEY_4, KeyMod::getDefaultModifier() }, "switch-to-tab-4" }, - { { KEY_5, KeyMod::getDefaultModifier() }, "switch-to-tab-5" }, - { { KEY_6, KeyMod::getDefaultModifier() }, "switch-to-tab-6" }, - { { KEY_7, KeyMod::getDefaultModifier() }, "switch-to-tab-7" }, - { { KEY_8, KeyMod::getDefaultModifier() }, "switch-to-tab-8" }, - { { KEY_9, KeyMod::getDefaultModifier() }, "switch-to-tab-9" }, - { { KEY_0, KeyMod::getDefaultModifier() }, "switch-to-last-tab" }, + { { KEY_1, DefaultSwitchToTabModifier }, "switch-to-tab-1" }, + { { KEY_2, DefaultSwitchToTabModifier }, "switch-to-tab-2" }, + { { KEY_3, DefaultSwitchToTabModifier }, "switch-to-tab-3" }, + { { KEY_4, DefaultSwitchToTabModifier }, "switch-to-tab-4" }, + { { KEY_5, DefaultSwitchToTabModifier }, "switch-to-tab-5" }, + { { KEY_6, DefaultSwitchToTabModifier }, "switch-to-tab-6" }, + { { KEY_7, DefaultSwitchToTabModifier }, "switch-to-tab-7" }, + { { KEY_8, DefaultSwitchToTabModifier }, "switch-to-tab-8" }, + { { KEY_9, DefaultSwitchToTabModifier }, "switch-to-tab-9" }, + { { KEY_0, DefaultSwitchToTabModifier }, "switch-to-last-tab" }, { { KEY_LEFT, KEYMOD_LALT }, "editor-go-back" }, { { KEY_RIGHT, KEYMOD_LALT }, "editor-go-forward" }, }; @@ -1518,6 +1528,18 @@ bool UICodeEditorSplitter::isWidgetInAnyWidget( UIWidget* checkWidget ) const { return found; } +UITab* UICodeEditorSplitter::getTabFromWidget( UIWidget* checkWidget ) const { + for ( auto tabWidget : mTabWidgets ) { + size_t tabCount = tabWidget->getTabCount(); + for ( size_t i = 0; i < tabCount; i++ ) { + UITab* tab = tabWidget->getTab( i ); + if ( tab->getOwnedWidget() == checkWidget ) + return tab; + } + } + return nullptr; +} + bool UICodeEditorSplitter::curEditorExists() const { bool found = false; forEachEditorStoppable( [&]( UICodeEditor* editor ) { diff --git a/src/eepp/ui/tools/uidiffview.cpp b/src/eepp/ui/tools/uidiffview.cpp index 17f54c518..e33ee1a5c 100644 --- a/src/eepp/ui/tools/uidiffview.cpp +++ b/src/eepp/ui/tools/uidiffview.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -294,7 +295,9 @@ std::vector UIDiffView::splitDiff( const std::string& multiFileDiff return diffs; } -UIDiffView::UIDiffView() : UIWidget( "diffview" ) { +UIDiffView::UIDiffView() : + UIWidget( "diffview" ), + WidgetCommandExecuter( KeyBindings{ getUISceneNode()->getWindow()->getInput() } ) { setFlags( UI_AUTO_SIZE ); createEditor( mEditor, mPlugin ); createEditor( mLeftEditor, mLeftPlugin ); diff --git a/src/eepp/ui/tools/uidocfindreplace.cpp b/src/eepp/ui/tools/uidocfindreplace.cpp index 21dde16ac..1e0f1bbeb 100644 --- a/src/eepp/ui/tools/uidocfindreplace.cpp +++ b/src/eepp/ui/tools/uidocfindreplace.cpp @@ -109,11 +109,11 @@ const char DOC_FIND_REPLACE_CSS[] = R"css( const char DOC_FIND_REPLACE_XML[] = R"xml( -