diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index b2c8fb7..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,177 +0,0 @@ -# Copilot Instructions for ktsu ImGui Suite - -## Project Overview - -This is a comprehensive C#/.NET suite of libraries for building ImGui applications. The project consists of four main libraries: -- **ImGui.App** - Application foundation and scaffolding -- **ImGui.Widgets** - Custom UI components and controls -- **ImGui.Popups** - Modal dialogs and popup systems -- **ImGui.Styler** - Theming and styling utilities - -## Technology Stack - -- **.NET 9.0** - Target framework -- **C#** with latest language features -- **Dear ImGui** via Hexa.NET.ImGui bindings -- **Silk.NET** for cross-platform windowing -- **MSTest** for unit testing -- **PowerShell** for build automation (PSBuild.psm1) - -## Code Style and Conventions - -### C# Coding Standards - -Use tabs for indentation in C# files (not spaces). - -Follow these naming conventions: -- PascalCase for types, methods, properties, and public members -- Interfaces must start with `I` prefix -- Do not use `this.` qualifier -- Use language keywords instead of framework types (e.g., `int` not `Int32`) - -Expression preferences: -- Use object and collection initializers -- Use explicit tuple names -- Prefer auto-properties -- Use pattern matching over `is` with cast -- Use switch expressions where appropriate -- No `var` - always use explicit types - -File organization: -- Use file-scoped namespaces -- Place using directives inside namespace -- One class per file - -Code structure: -- Always use braces for control flow statements -- No top-level statements - use traditional program structure -- Use primary constructors when appropriate -- Expression-bodied members only when on single line -- New line before opening braces for all constructs - -### Code Quality Rules - -All analyzer diagnostics are treated as errors. Key rules: -- Validate all public method arguments (CA1062) -- Implement IDisposable correctly (CA1063) -- Avoid catching general exception types (CA1031) -- Use StringComparison explicitly (CA1307) -- Avoid excessive complexity (CA1502) -- All code must pass static analysis - -### File Headers - -All C# source files must include this header: -```csharp -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. -``` - -## Build and Testing - -### Building - -Use the PSBuild pipeline for builds: -```powershell -Import-Module ./scripts/PSBuild.psm1 -``` - -Or use standard .NET commands: -```bash -dotnet restore -dotnet build -``` - -### Testing - -Run tests using: -```bash -dotnet test -``` - -Tests use MSTest framework and are located in the `tests/` directory. - -All code changes should include appropriate unit tests unless there is no existing test infrastructure for that component. - -### CI/CD - -The project uses GitHub Actions with a custom PowerShell build pipeline (`.github/workflows/dotnet.yml`): -- Builds, tests, and releases automatically -- Uses SonarQube for code analysis -- Generates code coverage reports -- Publishes to NuGet -- Runs on Windows - -## Project Structure - -``` -ImGui.App/ - Core application framework -ImGui.Widgets/ - UI widgets and components -ImGui.Popups/ - Modal and popup systems -ImGui.Styler/ - Theme and styling utilities -examples/ - Demo applications -tests/ - Unit tests -scripts/ - Build scripts (PowerShell) -.github/ - GitHub Actions workflows -``` - -## Package Management - -Dependencies are managed centrally via `Directory.Packages.props` for version consistency across all projects. - -Use NuGet for package management: -```bash -dotnet add package -``` - -## Documentation - -Update README.md files when making changes to APIs or adding new features. - -Each library has its own README with: -- Feature documentation -- API examples -- Usage patterns - -## Dependencies and Security - -Before adding new NuGet packages, verify they are secure and well-maintained. - -The project has scheduled security scans via GitHub Actions. - -## Git Workflow - -- Main development happens on `develop` branch -- Releases are made to `main` branch -- Follow conventional commit messages -- All commits must pass CI checks -- CI runs on push to main/develop and on all PRs - -## Common Tasks - -### Adding a new widget -1. Add class to `ImGui.Widgets/` -2. Follow existing widget patterns -3. Add demo usage to `examples/ImGuiWidgetsDemo/` -4. Update `ImGui.Widgets/README.md` -5. Add unit tests if applicable - -### Adding a new theme -1. Add theme definition to `ImGui.Styler/` -2. Follow existing theme structure -3. Test in `examples/ImGuiStylerDemo/` -4. Update theme gallery documentation - -### Modifying application framework -1. Changes to `ImGui.App/` affect all consumers -2. Ensure backward compatibility when possible -3. Update breaking changes in CHANGELOG.md -4. Test with all example applications - -## Notes - -- This is a cross-platform library targeting Windows, macOS, and Linux -- Performance is critical - use efficient patterns -- The project uses Dear ImGui's immediate mode paradigm -- Font and texture management requires careful resource handling diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 687f2f0..d589f71 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,4 +16,4 @@ updates: - "System.*" Silk: patterns: - - "Silk.NET.*" + - "Silk.NET.*" \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index d817799..d5834f7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,7 +26,7 @@ jobs: build: name: Build, Test & Release runs-on: windows-latest - timeout-minutes: 20 + timeout-minutes: 15 permissions: contents: write # For creating releases and committing metadata packages: write # For publishing packages @@ -35,7 +35,6 @@ jobs: version: ${{ steps.pipeline.outputs.version }} release_hash: ${{ steps.pipeline.outputs.release_hash }} should_release: ${{ steps.pipeline.outputs.should_release }} - skipped_release: ${{ steps.pipeline.outputs.skipped_release }} steps: - name: Set up JDK 17 @@ -60,11 +59,6 @@ jobs: cache: true cache-dependency-path: "**/*.csproj" - - name: Install dotnet-coverage - shell: pwsh - run: | - dotnet tool install --global dotnet-coverage - - name: Cache SonarQube Cloud packages if: ${{ env.SONAR_TOKEN != '' }} uses: actions/cache@v4 @@ -90,7 +84,7 @@ jobs: if: ${{ env.SONAR_TOKEN != '' && steps.cache-sonar-scanner.outputs.cache-hit != 'true' }} env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: pwsh + shell: powershell run: | New-Item -Path .\.sonar\scanner -ItemType Directory dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner @@ -99,9 +93,9 @@ jobs: if: ${{ env.SONAR_TOKEN != '' }} env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: pwsh + shell: powershell run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.coverage.exclusions="**/*Tests/**/*,**/*Test/**/*,**/obj/**/*,**/*.dll" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml + .\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="coverage/coverage.opencover.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll" /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx" /d:sonar.verbose=true - name: Run PSBuild Pipeline id: pipeline @@ -121,7 +115,6 @@ jobs: -GitHubRepo "${{ github.repository }}" ` -GithubToken "${{ github.token }}" ` -NuGetApiKey "${{ secrets.NUGET_KEY }}" ` - -KtsuPackageKey "${{ secrets.KTSU_PACKAGE_KEY }}" ` -WorkspacePath "${{ github.workspace }}" ` -ExpectedOwner "ktsu-dev" ` -ChangelogFile "CHANGELOG.md" ` @@ -147,21 +140,17 @@ jobs: "release_hash=$($buildConfig.Data.ReleaseHash)" >> $env:GITHUB_OUTPUT "should_release=$($buildConfig.Data.ShouldRelease)" >> $env:GITHUB_OUTPUT - if ($buildConfig.Data.SkippedRelease) { - "skipped_release=true" >> $env:GITHUB_OUTPUT - } - - name: End SonarQube - if: env.SONAR_TOKEN != '' && steps.pipeline.outputs.skipped_release != 'true' + if: ${{ env.SONAR_TOKEN != '' }} env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: pwsh + shell: powershell run: | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - name: Upload Coverage Report uses: actions/upload-artifact@v4 - if: always() && steps.pipeline.outputs.skipped_release != 'true' + if: always() with: name: coverage-report path: | @@ -171,7 +160,7 @@ jobs: winget: name: Update Winget Manifests needs: build - if: needs.build.outputs.should_release == 'true' && needs.build.outputs.skipped_release != 'true' + if: needs.build.outputs.should_release == 'true' runs-on: windows-latest timeout-minutes: 10 permissions: @@ -208,7 +197,7 @@ jobs: security: name: Security Scanning needs: build - if: needs.build.outputs.should_release == 'true' && needs.build.outputs.skipped_release != 'true' + if: needs.build.outputs.should_release == 'true' runs-on: windows-latest timeout-minutes: 10 permissions: diff --git a/.github/workflows/update-sdks.yml b/.github/workflows/update-sdks.yml index 36fb371..05febb5 100644 --- a/.github/workflows/update-sdks.yml +++ b/.github/workflows/update-sdks.yml @@ -59,7 +59,7 @@ jobs: # Function to get latest version from NuGet function Get-LatestNuGetVersion { param([string]$PackageId) - + try { Write-Host "Checking NuGet for package: $PackageId" # NuGet API requires lowercase package names @@ -84,11 +84,11 @@ jobs: [string]$SdkName, [string]$NewVersion ) - + $content = Get-Content $FilePath -Raw $pattern = "Sdk=`"$SdkName/[\d\.]+" $replacement = "Sdk=`"$SdkName/$NewVersion" - + if ($content -match $pattern) { $newContent = $content -replace $pattern, $replacement if ($content -ne $newContent) { @@ -107,11 +107,11 @@ jobs: [string]$SdkName, [string]$NewVersion ) - + try { $json = Get-Content $FilePath -Raw | ConvertFrom-Json $updated = $false - + # Check if msbuild-sdks section exists if ($json.PSObject.Properties.Name -contains "msbuild-sdks") { if ($json."msbuild-sdks".PSObject.Properties.Name -contains $SdkName) { @@ -122,7 +122,7 @@ jobs: } } } - + if ($updated) { $json | ConvertTo-Json -Depth 10 | Set-Content -Path $FilePath -NoNewline Write-Host "Updated $FilePath : $SdkName -> $NewVersion" @@ -147,7 +147,7 @@ jobs: if ($content -match 'Sdk="(ktsu\.Sdk\.\w+)/([\d\.]+)"') { $sdkName = $matches[1] $currentVersion = $matches[2] - + if (-not $sdkVersions.ContainsKey($sdkName)) { $sdkVersions[$sdkName] = $currentVersion } @@ -162,7 +162,7 @@ jobs: foreach ($property in $json."msbuild-sdks".PSObject.Properties) { $sdkName = $property.Name $currentVersion = $property.Value - + # Only track ktsu SDKs if ($sdkName -like "ktsu.Sdk.*") { if (-not $sdkVersions.ContainsKey($sdkName)) { @@ -191,7 +191,7 @@ jobs: foreach ($sdk in $sdkVersions.Keys) { Write-Host "Checking for updates to $sdk..." $latestVersion = Get-LatestNuGetVersion -PackageId $sdk - + if ($latestVersion -and $latestVersion -ne $sdkVersions[$sdk]) { $updates[$sdk] = $latestVersion Write-Host " Update available: $($sdkVersions[$sdk]) -> $latestVersion" @@ -231,7 +231,7 @@ jobs: if ($hasUpdates) { "has_updates=true" >> $env:GITHUB_OUTPUT - + # Create summary of changes $summary = "SDK Updates:`n" foreach ($sdk in $updates.Keys) { @@ -313,4 +313,4 @@ jobs: Write-Host "## â„šī¸ No SDK Updates Available" >> $env:GITHUB_STEP_SUMMARY Write-Host "" >> $env:GITHUB_STEP_SUMMARY Write-Host "All ktsu SDKs are already up to date." >> $env:GITHUB_STEP_SUMMARY - } + } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d4fbb60 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/ImGuiAppDemo/bin/Debug/net9.0/ktsu.ImGuiAppDemo.dll", + "args": [], + "cwd": "${workspaceFolder}/ImGuiAppDemo", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ee959f1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/ImGuiApp.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/ImGuiApp.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/ImGuiApp.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 958745b..326e546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,6 @@ -## v2.1.9 +## v2.1.3 -Changes since v2.1.9: -## v2.1.9 (patch) - -Changes since v2.1.8: - -- Fix missing package references ([@matt-edmondson](https://github.com/matt-edmondson)) -## v2.1.8 (patch) - -Changes since v2.1.7: - -- Enhance project structure and testing: Added new dependencies in Directory.Packages.props, introduced a new Tests project in the solution, and updated project references. Refactored namespaces for consistency across multiple files. Updated test configurations and example projects to align with the new structure. ([@matt-edmondson](https://github.com/matt-edmondson)) -- Initial combined commit ([@matt-edmondson](https://github.com/matt-edmondson)) -## v2.1.7 (patch) - -Changes since v2.1.6: - -- Fix NuGet package source URL in Invoke-NuGetPublish function: Updated the source URL to ensure correct package publishing to packages.ktsu.dev. ([@matt-edmondson](https://github.com/matt-edmondson)) -- Enhance .NET CI workflow: Added support for skipped releases in the GitHub Actions workflow. Updated conditions for SonarQube execution, coverage report upload, and Winget manifest updates to account for skipped releases, improving control over the release process. ([@matt-edmondson](https://github.com/matt-edmondson)) -## v2.1.6 (patch) - -Changes since v2.1.5: - -- Add Ktsu package key support in build configuration: Updated the .NET CI workflow and PowerShell script to include an optional Ktsu package key for publishing. Enhanced documentation for the new parameter and added conditional publishing logic for Ktsu.dev. ([@matt-edmondson](https://github.com/matt-edmondson)) -## v2.1.5 (patch) - -Changes since v2.1.4: - -- Implement modern DPI awareness handling in Windows: Updated ForceDpiAware to utilize the latest DPI awareness APIs for better compatibility with windowing libraries. Added fallback mechanisms for older Windows versions and enhanced NativeMethods with new DPI awareness context functions. ([@matt-edmondson](https://github.com/matt-edmondson)) -## v2.1.4 (patch) - -Changes since v2.1.3: - -- Enhance window position validation logic: Implemented performance optimizations to skip unnecessary checks when window position and size remain unchanged. Added methods for better multi-monitor support, ensuring windows are relocated when insufficiently visible. Updated tests to verify new behavior and performance improvements. ([@matt-edmondson](https://github.com/matt-edmondson)) +No significant changes detected since v2.1.3. ## v2.1.3 (patch) Changes since v2.1.2: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2b47fb2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,194 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ImGuiApp is a comprehensive .NET library that provides complete application scaffolding for Dear ImGui applications. It features advanced window management, DPI-aware rendering, performance optimization with throttling, advanced font handling with Unicode/emoji support, texture management with caching, debug tooling, and cross-platform compatibility. Built on Silk.NET for OpenGL and windowing, with Hexa.NET.ImGui for modern Dear ImGui bindings. + +## Architecture + +### Core Components +- **ImGuiApp.cs**: Main static class providing the public API, application lifecycle management, debug logging, and performance optimization +- **ImGuiAppConfig.cs**: Configuration object with comprehensive settings including performance tuning and lifecycle callbacks +- **ImGuiAppPerformanceSettings.cs**: Advanced performance settings for throttled rendering with sleep-based frame rate control +- **ImGuiController/**: Contains OpenGL abstraction layer with IGL interface, texture management, font handling, and shader utilities +- **FontAppearance.cs**: RAII wrapper for applying font styles using `using` statements with dynamic scaling support +- **FontHelper.cs**: Utility for advanced Unicode, emoji, and extended character range handling +- **UIScaler.cs**: Handles DPI-aware scaling calculations with pixel-perfect rendering +- **ForceDpiAware.cs**: Platform-specific DPI detection and awareness enforcement across Windows, Linux, and WSL +- **GdiPlusHelper.cs**: Graphics utilities for enhanced rendering capabilities + +### Key Design Patterns +- Static facade pattern for the main ImGuiApp class +- OpenGL abstraction through IGL interface for testability +- Resource management through IDisposable patterns with automatic cleanup +- Configuration-driven architecture with extensive delegate callbacks +- Performance optimization through sleep-based throttling with multi-condition evaluation +- Debug logging with automatic crash diagnostics + +### Recent Enhancements +- **Debug Logging**: Comprehensive logging system that creates debug files for troubleshooting crashes and performance issues +- **Performance Throttling**: Multi-condition frame rate throttling (focused/unfocused/idle/not visible) using sleep-based timing +- **Context Handling**: Automatic OpenGL context change detection with texture reloading +- **Memory Optimization**: Pool-based memory management for textures and improved font memory handling +- **Unicode & Emoji Support**: Built-in support enabled by default with configurable character ranges +- **Dynamic Font Scaling**: Improved font rendering with crisp scaling at multiple DPI levels + +## Build and Development Commands + +### Build +```bash +dotnet build ImGuiApp.sln +``` + +### Test +```bash +dotnet test ImGuiApp.Test/ +``` + +### Run Demo +```bash +dotnet run --project ImGuiAppDemo/ +``` + +### Package +```bash +dotnet pack ImGuiApp/ -o staging/ +``` + +### Run Single Test +```bash +dotnet test ImGuiApp.Test/ --filter "TestMethodName" +``` + +### Build in Release Mode +```bash +dotnet build ImGuiApp.sln -c Release +``` + +### PowerShell Build System +The project uses a comprehensive PowerShell build system located in `scripts/`: +- Use `Import-Module ./scripts/PSBuild.psm1` to access build functions +- `Invoke-CIPipeline` runs the complete CI pipeline with versioning, build, test, and packaging +- Supports semantic versioning based on git history and commit message tags ([major], [minor], [patch], [pre]) + +## Project Structure + +### Main Library (`ImGuiApp/`) +- Uses ktsu.Sdk.Lib SDK with .NET 9+ multi-targeting +- Dependencies: Hexa.NET.ImGui, Silk.NET, SixLabors.ImageSharp, ktsu utilities (Invoker, StrongPaths, Extensions, ScopedAction) +- Allows unsafe blocks for OpenGL operations and memory management +- Embedded resources for fonts including Nerd Font and NotoEmoji in `Resources/` + +### Test Project (`ImGuiApp.Test/`) +- Uses ktsu.Sdk.Test SDK with MSTest framework +- Comprehensive test coverage across multiple test classes: + - `ImGuiAppCoreTests.cs`: Core functionality tests + - `ImGuiAppDataStructureTests.cs`: Configuration and data structure tests + - `FontAndUITests.cs`: Font management and UI scaling tests + - `ErrorHandlingAndEdgeCaseTests.cs`: Exception handling and edge cases + - `PlatformSpecificTests.cs`: Cross-platform compatibility tests + - `AdvancedCoverageTests.cs`: Advanced feature coverage +- Mock implementations for OpenGL testing (MockGL.cs, TestOpenGLProvider.cs) +- Tests for DPI awareness, font management, performance throttling, and debug features + +### Demo Application (`ImGuiAppDemo/`) +- Comprehensive demonstration with tabbed interface: + - Unicode & Emoji showcase + - Widgets & Layout demonstrations + - Graphics & Plotting examples + - Nerd Font icons gallery +- Performance monitoring accessible through Debug menu (Debug > Show Performance Monitor) + +## Key Implementation Details + +### Performance Optimization +- Sleep-based frame rate throttling with Thread.Sleep for precise timing control +- Multi-condition evaluation (focused/unfocused/idle/not visible) with "lowest wins" logic +- Automatic user input detection for idle state management +- Resource conservation with ultra-low frame rates when minimized + +### Debug Features +- Automatic debug log creation (`ImGuiApp_Debug.log` on desktop) +- Comprehensive logging during initialization, font loading, and error conditions +- Built-in debug menu with ImGui Demo, Metrics, and Performance Monitor windows +- Real-time performance monitoring with FPS graphs and throttling visualization + +### DPI Handling +- Cross-platform DPI detection in ForceDpiAware.cs with Windows, Linux, and WSL support +- Automatic scaling calculations through UIScaler with pixel-perfect rendering +- Dynamic font scaling based on DPI changes +- Platform-specific implementations with Wayland support + +### Font Management +- Embedded font resources (Nerd Font and NotoEmoji) +- Dynamic font loading system with multiple size support (10, 12, 14, 16, 18, 20, 24, 32, 48 pt) +- Unicode and emoji support enabled by default +- FontHelper utility for extended character ranges and glyph management +- Memory-efficient font loading with pre-allocated handles + +### Texture Management +- Concurrent dictionary-based texture caching +- Automatic texture reloading on OpenGL context changes +- Pool-based memory management for improved performance +- Support for various image formats through SixLabors.ImageSharp + +### OpenGL Abstraction +- IGL interface abstracts OpenGL calls for testing +- WindowOpenGLFactory creates platform-appropriate GL contexts +- Automatic context change detection and handling +- Cross-platform compatibility (Windows, Linux) + +## Testing Approach + +Tests use MSTest framework with comprehensive mock OpenGL implementations. Key test categories: +- Core application lifecycle and configuration validation +- DPI detection across multiple platforms +- Font loading, scaling, and Unicode support +- Performance throttling and idle detection +- OpenGL abstraction layer and context handling +- Error handling and edge cases +- Memory management and cleanup + +When writing tests, use the existing mock patterns in TestOpenGLProvider.cs and MockGL.cs. The test suite is organized into focused test classes for better maintainability. + +## Version Management + +The project uses automated semantic versioning with comprehensive changelog generation: +- Version tags in commit messages control increments +- Public API changes automatically trigger minor version bumps +- VERSION.md, CHANGELOG.md, and other metadata files are auto-generated +- Uses git history analysis for version calculation with detailed release notes + +## Key File Locations and Patterns + +### Main Components +- `ImGuiApp/ImGuiApp.cs`: Main static API class with debug logging and performance optimization (ImGuiApp:32) +- `ImGuiApp/ImGuiController/IGL.cs`: OpenGL abstraction interface +- `ImGuiApp/ImGuiController/ImGuiController.cs`: Core ImGui controller implementation +- `ImGuiApp/FontAppearance.cs`: RAII font management with dynamic scaling (FontAppearance:14) +- `ImGuiApp/FontHelper.cs`: Unicode, emoji, and glyph range utilities +- `ImGuiApp/UIScaler.cs`: DPI-aware scaling utility with pixel-perfect rendering (UIScaler:16) +- `ImGuiApp/ForceDpiAware.cs`: Cross-platform DPI detection with Wayland support (ForceDpiAware:17) + +### Configuration and Data +- `ImGuiApp/ImGuiAppConfig.cs`: Application configuration with performance settings (ImGuiAppConfig:12) +- `ImGuiApp/ImGuiAppPerformanceSettings.cs`: Advanced performance tuning configuration +- `ImGuiApp/ImGuiAppWindowState.cs`: Window state management +- `ImGuiApp/ImGuiAppTextureInfo.cs`: Texture information and management +- `ImGuiApp/Resources/`: Embedded font resources (Nerd Font and NotoEmoji) + +### Testing Infrastructure +- `ImGuiApp.Test/MockGL.cs`: Mock OpenGL implementation for testing +- `ImGuiApp.Test/TestOpenGLProvider.cs`: Test OpenGL provider +- `ImGuiApp.Test/TestHelpers.cs`: Common test utilities +- `ImGuiApp.Test/ImGuiAppCoreTests.cs`: Core application functionality tests +- `ImGuiApp.Test/FontAndUITests.cs`: Font and UI scaling tests +- `ImGuiApp.Test/ErrorHandlingAndEdgeCaseTests.cs`: Exception handling tests +- `ImGuiApp.Test/AdvancedCoverageTests.cs`: Advanced feature coverage + +### Build System +- `scripts/PSBuild.psm1`: PowerShell build automation module +- Uses ktsu.Sdk.Lib, ktsu.Sdk.Test, and ktsu.Sdk.App SDKs +- Supports automatic font updates via `scripts/Update-NerdFont.ps1` diff --git a/ImGui.App/DESCRIPTION.md b/DESCRIPTION.md similarity index 100% rename from ImGui.App/DESCRIPTION.md rename to DESCRIPTION.md diff --git a/Directory.Packages.props b/Directory.Packages.props index e420dd5..6e4cddf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,26 +3,45 @@ true - - - - - - - - - - - - - - - - - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + diff --git a/ImGui.App/CompatibilitySuppressions.xml b/ImGui.App/CompatibilitySuppressions.xml deleted file mode 100644 index 2d32f4b..0000000 --- a/ImGui.App/CompatibilitySuppressions.xml +++ /dev/null @@ -1,724 +0,0 @@ -īģŋ - - - - CP0001 - T:System.Diagnostics.CodeAnalysis.ExperimentalAttribute - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Runtime.CompilerServices.CollectionBuilderAttribute - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureGuardAttribute - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Runtime.CompilerServices.ParamCollectionAttribute - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0001 - T:System.Threading.Lock - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0008 - T:ktsu.ImGui.App.ImGuiController.TextureCoordinate - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0008 - T:System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0008 - T:System.IO.UnixFileMode - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_UrlFormat:[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_UrlFormat(System.String):[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.App.dll - lib/net9.0/ktsu.ImGui.App.dll - - - CP0016 - F:System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute.CallConvs:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - F:System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute.EntryPoint:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.FontHelper.AddCustomFont(Hexa.NET.ImGui.ImGuiIOPtr,System.Byte[],System.Single,System.UInt32*,System.Boolean)$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiApp.TryGetTexture(ktsu.Semantics.Paths.AbsoluteFilePath,ktsu.ImGui.App.ImGuiAppTextureInfo@)$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiApp.TryGetTexture(System.String,ktsu.ImGui.App.ImGuiAppTextureInfo@)$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiAppConfig.get_FrameWrapperFactory->System.Func<ktsu.ScopedAction.ScopedAction?>:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiAppConfig.set_FrameWrapperFactory(System.Func{ktsu.ScopedAction.ScopedAction})$0:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.GLWrapper.#ctor(Silk.NET.OpenGL.GL):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.GLWrapper.get_UnderlyingGL:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig.#ctor(System.String,System.Int32,System.Func{Hexa.NET.ImGui.ImGuiIOPtr,System.IntPtr})$2:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig.Equals(System.Object):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig.get_GetGlyphRange:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:ktsu.ImGui.App.ImGuiController.IOpenGLProvider.GetGL:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Collections.Generic.KeyValuePair.Create``2(``0,``1)->System.Collections.Generic.KeyValuePair<TKey, TValue>:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Collections.Generic.KeyValuePair.Create``2(``0,``1):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Collections.Generic.KeyValuePair.Create``2(``0,``1)<0>:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Collections.Generic.KeyValuePair.Create``2(``0,``1)<1>:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.#ctor(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes,System.String,System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.#ctor(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes,System.Type):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.#ctor(System.String,System.String,System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.#ctor(System.String,System.Type):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.get_Url:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.set_Url(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.get_Url:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.set_Url(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.#ctor(System.String,System.Object[])$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.get_Arguments->object?[]:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute.#ctor(System.String,System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute.get_Category:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute.get_CheckId:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Index.Equals(System.Object):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Index.ToString:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Range.Equals(System.Object):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Range.ToString:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Reflection.NullabilityInfo.get_ElementType:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.ObsoletedOSPlatformAttribute.#ctor(System.String,System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.ObsoletedOSPlatformAttribute.#ctor(System.String,System.String)$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.ObsoletedOSPlatformAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.SupportedOSPlatformAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.SupportedOSPlatformGuardAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.TargetPlatformAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.UnsupportedOSPlatformAttribute.#ctor(System.String,System.String)$1:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.UnsupportedOSPlatformAttribute.get_Message:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - M:System.Runtime.Versioning.UnsupportedOSPlatformGuardAttribute.#ctor(System.String):[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:ktsu.ImGui.App.ImGuiAppConfig.FrameWrapperFactory:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:ktsu.ImGui.App.ImGuiController.GLWrapper.UnderlyingGL:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig.GetGlyphRange:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.Url:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.Url:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Arguments:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute.Category:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute.CheckId:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Reflection.NullabilityInfo.ElementType:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - P:System.Runtime.Versioning.UnsupportedOSPlatformAttribute.Message:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.FontAppearance:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.FontAppearance:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiApp:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiApp:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiAppConfig:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiAppConfig:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiAppTextureInfo:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiAppTextureInfo:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.ImGuiFontConfig:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.IOpenGLFactory:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.OpenGLProvider:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.OpenGLProvider:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.WindowOpenGLFactory:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:ktsu.ImGui.App.ImGuiController.WindowOpenGLFactory:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.ConstantExpectedAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.ConstantExpectedAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.MemberNotNullAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.MemberNotNullAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.UnreachableException:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Diagnostics.UnreachableException:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Reflection.NullabilityInfo:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Reflection.NullabilityInfo:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Reflection.NullabilityInfoContext:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Reflection.NullabilityInfoContext:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.CompilerServices.RequiredMemberAttribute:[T:System.ComponentModel.EditorBrowsableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.ObsoletedOSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.ObsoletedOSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.OSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.OSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.RequiresPreviewFeaturesAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.RequiresPreviewFeaturesAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - - CP0016 - T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute] - lib/net7.0/ktsu.ImGui.App.dll - lib/net8.0/ktsu.ImGui.App.dll - - \ No newline at end of file diff --git a/ImGui.App/README.md b/ImGui.App/README.md deleted file mode 100644 index 607e49c..0000000 --- a/ImGui.App/README.md +++ /dev/null @@ -1,573 +0,0 @@ -# ktsu.ImGuiApp - -> A .NET library that provides application scaffolding for Dear ImGui, using Silk.NET and Hexa.NET.ImGui. - -[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiApp.svg)](https://www.nuget.org/packages/ktsu.ImGuiApp/) -[![License](https://img.shields.io/github/license/ktsu-dev/ImGuiApp.svg)](LICENSE.md) -[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.ImGuiApp.svg)](https://www.nuget.org/packages/ktsu.ImGuiApp/) -[![GitHub Stars](https://img.shields.io/github/stars/ktsu-dev/ImGuiApp?style=social)](https://github.com/ktsu-dev/ImGuiApp/stargazers) - -## Introduction - -ImGuiApp is a .NET library that provides application scaffolding for [Dear ImGui](https://github.com/ocornut/imgui), using [Silk.NET](https://github.com/dotnet/Silk.NET) for OpenGL and window management and [Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui) for the ImGui bindings. It simplifies the creation of ImGui-based applications by abstracting away the complexities of window management, rendering, and input handling. - -## Features - -- **Simple API**: Create ImGui applications with minimal boilerplate code -- **Full Integration**: Seamless integration with Silk.NET for OpenGL and input handling -- **Window Management**: Automatic window state, rendering, and input handling -- **Performance Optimization**: Sleep-based throttled rendering with lowest-selection logic when unfocused, idle, or not visible to maximize resource savings -- **PID Frame Limiting**: Precision frame rate control using a PID controller with comprehensive auto-tuning capabilities for highly accurate target FPS achievement -- **DPI Awareness**: Built-in support for high-DPI displays and scaling -- **Font Management**: Flexible font loading system with customization options and dynamic scaling -- **Unicode & Emoji Support**: Built-in support for Unicode characters and emojis (enabled by default) -- **Texture Support**: Built-in texture management with caching and automatic cleanup for ImGui -- **Debug Logging**: Comprehensive debug logging system for troubleshooting crashes and performance issues -- **Context Handling**: Automatic OpenGL context change detection and texture reloading -- **Lifecycle Callbacks**: Customizable delegate callbacks for application events -- **Menu System**: Easy-to-use API for creating application menus -- **Positioning Guards**: Offscreen positioning checks to keep windows visible -- **Modern .NET**: Supports .NET 9 and newer -- **Active Development**: Open-source and actively maintained - -## Getting Started - -### Prerequisites - -- .NET 9.0 or later - -## Installation - -### Package Manager Console - -```powershell -Install-Package ktsu.ImGuiApp -``` - -### .NET CLI - -```bash -dotnet add package ktsu.ImGuiApp -``` - -### Package Reference - -```xml - -``` - -## Usage Examples - -### Basic Application - -Create a new class and call `ImGuiApp.Start()` with your application config: - -```csharp -using ktsu.ImGuiApp; -using Hexa.NET.ImGui; - -static class Program -{ - static void Main() - { - ImGuiApp.Start(new ImGuiAppConfig() - { - Title = "ImGuiApp Demo", - OnStart = () => { /* Initialization code */ }, - OnUpdate = delta => { /* Logic updates */ }, - OnRender = delta => { ImGui.Text("Hello, ImGuiApp!"); }, - OnAppMenu = () => - { - if (ImGui.BeginMenu("File")) - { - // Menu items - if (ImGui.MenuItem("Exit")) - { - ImGuiApp.Stop(); - } - ImGui.EndMenu(); - } - } - }); - } -} -``` - -### Custom Font Management - -Use the resource designer to add font files to your project, then load the fonts: - -```csharp -ImGuiApp.Start(new() -{ - Title = "ImGuiApp Demo", - OnRender = OnRender, - Fonts = new Dictionary - { - { nameof(Resources.MY_FONT), Resources.MY_FONT } - }, -}); -``` - -Or load the font data manually: - -```csharp -var fontData = File.ReadAllBytes("path/to/font.ttf"); -ImGuiApp.Start(new() -{ - Title = "ImGuiApp Demo", - OnRender = OnRender, - Fonts = new Dictionary - { - { "MyFont", fontData } - }, -}); -``` - -Then apply the font to ImGui using the `FontAppearance` class: - -```csharp -private static void OnRender(float deltaTime) -{ - ImGui.Text("Hello, I am normal text!"); - - using (new FontAppearance("MyFont", 24)) - { - ImGui.Text("Hello, I am BIG fancy text!"); - } - - using (new FontAppearance(32)) - { - ImGui.Text("Hello, I am just huge text!"); - } - - using (new FontAppearance("MyFont")) - { - ImGui.Text("Hello, I am somewhat fancy!"); - } -} -``` - -### Unicode and Emoji Support - -ImGuiApp automatically includes support for Unicode characters and emojis. This feature is **enabled by default**, so you can use extended characters without any configuration: - -```csharp -private static void OnRender(float deltaTime) -{ - ImGui.Text("Basic ASCII: Hello World!"); - ImGui.Text("Accented characters: cafÊ, naïve, rÊsumÊ"); - ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ â‰Ĩ Âą × Ãˇ ∂ ∑"); - ImGui.Text("Currency symbols: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ"); - ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕"); - ImGui.Text("Emojis (if font supports): 😀 🚀 🌟 đŸ’ģ 🎨 🌈"); -} -``` - -**Note**: Character display depends on your font's Unicode support. Most modern fonts include extended Latin characters and symbols, but emojis require specialized fonts. - -To disable Unicode support (ASCII only), set `EnableUnicodeSupport = false`: - -```csharp -ImGuiApp.Start(new() -{ - Title = "ASCII Only App", - EnableUnicodeSupport = false, // Disables Unicode support - // ... other settings -}); -``` - -### Texture Management - -Load and manage textures with the built-in texture management system: - -```csharp -private static void OnRender(float deltaTime) -{ - // Load texture from file path - var textureInfo = ImGuiApp.GetOrLoadTexture("path/to/texture.png"); - - // Use the texture in ImGui (using the new TextureRef API for Hexa.NET.ImGui) - ImGui.Image(textureInfo.TextureRef, new Vector2(128, 128)); - - // Clean up when done (optional - textures are cached and managed automatically) - ImGuiApp.DeleteTexture(textureInfo); -} -``` - -### PID Frame Limiting - -ImGuiApp features a sophisticated **PID (Proportional-Integral-Derivative) controller** for precise frame rate limiting. This system provides highly accurate target FPS control that learns and adapts to your system's characteristics. - -#### Key Features - -- **High-Precision Timing**: Hybrid sleep system combining `Thread.Sleep()` for coarse delays with spin-waiting for sub-millisecond accuracy -- **PID Controller**: Advanced control algorithm that learns from frame timing errors and dynamically adjusts sleep times -- **Comprehensive Auto-Tuning**: Multi-phase tuning procedure that automatically finds optimal PID parameters for your system -- **VSync Independence**: Works independently of monitor refresh rates for any target FPS -- **Real-Time Diagnostics**: Built-in performance monitoring and tuning visualization - -#### Optimized Defaults - -ImGuiApp comes pre-configured with optimal PID parameters derived from comprehensive auto-tuning: - -- **Kp: 1.800** - Proportional gain for current error response -- **Ki: 0.048** - Integral gain for accumulated error correction -- **Kd: 0.237** - Derivative gain for predictive adjustment - -These defaults provide excellent frame timing accuracy out-of-the-box for most systems. - -#### Configuration - -Configure frame limiting through `ImGuiAppPerformanceSettings`: - -```csharp -ImGuiApp.Start(new ImGuiAppConfig -{ - Title = "PID Frame Limited App", - OnRender = OnRender, - PerformanceSettings = new ImGuiAppPerformanceSettings - { - EnableThrottledRendering = true, - FocusedFps = 30.0, // Target 30 FPS when focused - UnfocusedFps = 5.0, // Target 5 FPS when unfocused - IdleFps = 10.0, // Target 10 FPS when idle - NotVisibleFps = 2.0, // Target 2 FPS when minimized - EnableIdleDetection = true, - IdleTimeoutSeconds = 30.0 // Idle after 30 seconds - } -}); -``` - -#### Auto-Tuning Procedure - -For maximum accuracy, ImGuiApp includes a comprehensive **3-phase auto-tuning system**: - -1. **Coarse Phase** (8s per test): Tests 24 parameter combinations to find the general optimal range -2. **Fine Phase** (12s per test): Tests 25 refined parameters around the best coarse result -3. **Precision Phase** (15s per test): Final optimization with 9 precision-focused parameters - -**Total tuning time**: ~12-15 minutes for maximum accuracy - -Access auto-tuning through the **Debug > Show Performance Monitor** menu, which provides: -- Real-time tuning progress visualization -- Performance metrics (Average Error, Max Error, Stability, Score) -- Interactive tuning controls and results display -- Live FPS graphs showing PID controller performance - -#### Technical Details - -The PID controller works by: -- **Measuring** actual frame times vs. target frame times -- **Calculating** error using smoothed measurements to reduce noise -- **Adjusting** sleep duration using PID mathematics: `output = Kp×error + Ki×âˆĢerror + Kd×Δerror` -- **Learning** from past performance to minimize future timing errors - -The system automatically: -- Disables VSync to prevent interference with custom frame limiting -- Pauses throttling during auto-tuning for accurate measurements -- Uses integral windup prevention to maintain stability -- Applies high-precision sleep for sub-millisecond timing accuracy - -### Full Application with Multiple Windows - -```csharp -using ktsu.ImGuiApp; -using Hexa.NET.ImGui; -using System.Numerics; - -class Program -{ - private static bool _showDemoWindow = true; - private static bool _showCustomWindow = true; - - static void Main() - { - ImGuiApp.Start(new ImGuiAppConfig - { - Title = "Advanced ImGuiApp Demo", - InitialWindowState = new ImGuiAppWindowState - { - Size = new Vector2(1280, 720), - Pos = new Vector2(100, 100) - }, - OnStart = OnStart, - OnUpdate = OnUpdate, - OnRender = OnRender, - OnAppMenu = OnAppMenu, - }); - } - - private static void OnStart() - { - // Initialize your application state - Console.WriteLine("Application started"); - } - - private static void OnUpdate(float deltaTime) - { - // Update your application state - // This runs before rendering each frame - } - - private static void OnRender(float deltaTime) - { - // ImGui demo window - if (_showDemoWindow) - ImGui.ShowDemoWindow(ref _showDemoWindow); - - // Custom window - if (_showCustomWindow) - { - ImGui.Begin("Custom Window", ref _showCustomWindow); - - ImGui.Text($"Frame time: {deltaTime * 1000:F2} ms"); - ImGui.Text($"FPS: {1.0f / deltaTime:F1}"); - - if (ImGui.Button("Click Me")) - Console.WriteLine("Button clicked!"); - - ImGui.ColorEdit3("Background Color", ref _backgroundColor); - - ImGui.End(); - } - } - - private static void OnAppMenu() - { - if (ImGui.BeginMenu("File")) - { - if (ImGui.MenuItem("Exit")) - ImGuiApp.Stop(); - - ImGui.EndMenu(); - } - - if (ImGui.BeginMenu("Windows")) - { - ImGui.MenuItem("Demo Window", string.Empty, ref _showDemoWindow); - ImGui.MenuItem("Custom Window", string.Empty, ref _showCustomWindow); - ImGui.EndMenu(); - } - } - - private static Vector3 _backgroundColor = new Vector3(0.45f, 0.55f, 0.60f); -} -``` - -## API Reference - -### `ImGuiApp` Static Class - -The main entry point for creating and managing ImGui applications. - -#### Properties - -| Name | Type | Description | -|------|------|-------------| -| `WindowState` | `ImGuiAppWindowState` | Gets the current state of the application window | -| `Invoker` | `Invoker` | Gets an instance to delegate tasks to the window thread | -| `IsFocused` | `bool` | Gets whether the application window is focused | -| `IsVisible` | `bool` | Gets whether the application window is visible | -| `IsIdle` | `bool` | Gets whether the application is currently idle | -| `ScaleFactor` | `float` | Gets the current DPI scale factor | - -#### Methods - -| Name | Parameters | Return Type | Description | -|------|------------|-------------|-------------| -| `Start` | `ImGuiAppConfig config` | `void` | Starts the ImGui application with the provided configuration | -| `Stop` | | `void` | Stops the running application | -| `GetOrLoadTexture` | `AbsoluteFilePath path` | `ImGuiAppTextureInfo` | Loads a texture from file or returns cached texture info if already loaded | -| `TryGetTexture` | `AbsoluteFilePath path, out ImGuiAppTextureInfo textureInfo` | `bool` | Attempts to get a cached texture by path | -| `DeleteTexture` | `uint textureId` | `void` | Deletes a texture and frees its resources | -| `DeleteTexture` | `ImGuiAppTextureInfo textureInfo` | `void` | Deletes a texture and frees its resources (convenience overload) | -| `CleanupAllTextures` | | `void` | Cleans up all loaded textures | -| `SetWindowIcon` | `string iconPath` | `void` | Sets the window icon using the specified icon file path | -| `EmsToPx` | `float ems` | `int` | Converts a value in ems to pixels based on current font size | -| `PtsToPx` | `int pts` | `int` | Converts a value in points to pixels based on current scale factor | -| `UseImageBytes` | `Image image, Action action` | `void` | Executes an action with temporary access to image bytes using pooled memory | - -### `ImGuiAppConfig` Class - -Configuration for the ImGui application. - -#### Properties - -| Name | Type | Default | Description | -|------|------|---------|-------------| -| `TestMode` | `bool` | `false` | Whether the application is running in test mode | -| `Title` | `string` | `"ImGuiApp"` | The window title | -| `IconPath` | `string` | `""` | The file path to the application window icon | -| `InitialWindowState` | `ImGuiAppWindowState` | `new()` | The initial state of the application window | -| `Fonts` | `Dictionary` | `[]` | Font name to font data mapping | -| `EnableUnicodeSupport` | `bool` | `true` | Whether to enable Unicode and emoji support | -| `SaveIniSettings` | `bool` | `true` | Whether ImGui should save window settings to imgui.ini | -| `PerformanceSettings` | `ImGuiAppPerformanceSettings` | `new()` | Performance settings for throttled rendering | -| `OnStart` | `Action` | `() => { }` | Called when the application starts | -| `FrameWrapperFactory` | `Func` | `() => null` | Factory for creating frame wrappers | -| `OnUpdate` | `Action` | `(delta) => { }` | Called each frame before rendering (param: delta time) | -| `OnRender` | `Action` | `(delta) => { }` | Called each frame for rendering (param: delta time) | -| `OnAppMenu` | `Action` | `() => { }` | Called each frame for rendering the application menu | -| `OnMoveOrResize` | `Action` | `() => { }` | Called when the application window is moved or resized | - -### `ImGuiAppPerformanceSettings` Class - -Configuration for performance optimization and throttled rendering. Uses a sophisticated **PID controller with high-precision timing** to achieve accurate target frame rates while maintaining system resource efficiency. The system combines Thread.Sleep for coarse delays with spin-waiting for sub-millisecond precision, and automatically disables VSync to prevent interference with custom frame limiting. - -#### Properties - -| Name | Type | Default | Description | -|------|------|---------|-------------| -| `EnableThrottledRendering` | `bool` | `true` | Enables/disables throttled rendering feature | -| `FocusedFps` | `double` | `30.0` | Target frame rate when the window is focused and active | -| `UnfocusedFps` | `double` | `5.0` | Target frame rate when the window is unfocused | -| `IdleFps` | `double` | `10.0` | Target frame rate when the application is idle (no user input) | -| `NotVisibleFps` | `double` | `2.0` | Target frame rate when the window is not visible (minimized or hidden) | -| `EnableIdleDetection` | `bool` | `true` | Enables/disables idle detection based on user input | -| `IdleTimeoutSeconds` | `double` | `30.0` | Time in seconds without user input before considering the app idle | - -#### Example Usage - -```csharp -ImGuiApp.Start(new ImGuiAppConfig -{ - Title = "My Application", - OnRender = OnRender, - PerformanceSettings = new ImGuiAppPerformanceSettings - { - EnableThrottledRendering = true, - FocusedFps = 60.0, // Custom higher rate when focused - UnfocusedFps = 15.0, // Custom rate when unfocused - IdleFps = 2.0, // Custom very low rate when idle - NotVisibleFps = 1.0, // Custom ultra-low rate when minimized - EnableIdleDetection = true, - IdleTimeoutSeconds = 10.0 // Custom idle timeout - } - // PID controller uses optimized defaults: Kp=1.8, Ki=0.048, Kd=0.237 - // For fine-tuning, use Debug > Show Performance Monitor > Start Auto-Tuning -}); -``` - -This feature automatically: -- Uses a **PID controller** with optimized defaults for highly accurate frame rate targeting -- Combines **Thread.Sleep** with **spin-waiting** for sub-millisecond timing precision -- Disables **VSync** automatically to prevent interference with custom frame limiting -- Detects when the window loses/gains focus and visibility state (minimized/hidden) -- Tracks user input (keyboard, mouse movement, clicks, scrolling) for idle detection -- Evaluates all applicable throttling conditions and selects the lowest frame rate -- Saves significant CPU and GPU resources without affecting user experience -- Provides instant transitions between different performance states -- Uses conservative defaults: 30 FPS focused, 5 FPS unfocused, 10 FPS idle, 2 FPS not visible - -The **PID controller** learns from timing errors and adapts to your system's characteristics, providing much more accurate frame rate control than simple sleep-based methods. The throttling system uses a "lowest wins" approach - if multiple conditions apply (e.g., unfocused + idle), the lowest frame rate is automatically selected for maximum resource savings. - -### `FontAppearance` Class - -A utility class for applying font styles using a using statement. - -#### Constructors - -| Constructor | Parameters | Description | -|-------------|------------|-------------| -| `FontAppearance` | `string fontName` | Creates a font appearance with the named font at default size | -| `FontAppearance` | `float fontSize` | Creates a font appearance with the default font at the specified size | -| `FontAppearance` | `string fontName, float fontSize` | Creates a font appearance with the named font at the specified size | - -### `ImGuiAppWindowState` Class - -Represents the state of the application window. - -#### Properties - -| Name | Type | Description | -|------|------|-------------| -| `Size` | `Vector2` | The size of the window | -| `Pos` | `Vector2` | The position of the window | -| `LayoutState` | `WindowState` | The layout state of the window (Normal, Maximized, etc.) | - -## Debug Features - -ImGuiApp includes comprehensive debug logging capabilities to help troubleshoot crashes and performance issues: - -### Debug Logging - -The application automatically creates debug logs on the desktop (`ImGuiApp_Debug.log`) when issues occur. These logs include: -- Window initialization steps -- OpenGL context creation -- Font loading progress -- Error conditions and exceptions - -### Debug Menu - -When using the `OnAppMenu` callback, ImGuiApp automatically adds a Debug menu with options to: -- Show ImGui Demo Window -- Show ImGui Metrics Window -- Show Performance Monitor (real-time FPS graphs and throttling visualization) - -### Performance Monitoring - -The core library includes a built-in performance monitor accessible via the debug menu. It provides: -- Real-time FPS tracking and visualization -- Throttling state monitoring (focused/unfocused/idle/not visible) -- Performance testing tips and interactive guidance -- Historical performance data graphing - -Access it through: **Debug > Show Performance Monitor** - -## Demo Application - -Check out the included demo project to see a comprehensive working example: - -1. Clone or download the repository -2. Open the solution in Visual Studio (or run dotnet build) -3. Start the ImGuiAppDemo project to see a feature-rich ImGui application -4. Explore the different tabs: - - **Unicode & Emojis**: Test character rendering with extended Unicode support - - **Widgets & Layout**: Comprehensive ImGui widget demonstrations - - **Graphics & Plotting**: Custom drawing and data visualization examples - - **Nerd Font Icons**: Browse and test various icon sets and glyphs -5. Use the debug menu to access additional features: - - **Debug > Show Performance Monitor**: Real-time FPS graph showing PID controller performance with comprehensive auto-tuning capabilities - - **Debug > Show ImGui Demo**: Official ImGui demo window - - **Debug > Show ImGui Metrics**: ImGui internal metrics and debugging info - -The **Performance Monitor** includes: -- **Live FPS graphs** that visualize frame rate changes as you focus/unfocus the window, let it go idle, or minimize it -- **PID Controller diagnostics** showing real-time proportional, integral, and derivative values -- **Comprehensive Auto-Tuning** with 3-phase optimization (Coarse, Fine, Precision phases) -- **Performance metrics** including Average Error, Max Error, Stability, and composite Score -- **Interactive tuning controls** to start/stop optimization and view detailed results - -Perfect for seeing both the throttling system and PID controller work in real-time! - -## Contributing - -Contributions are welcome! Here's how you can help: - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -Please make sure to update tests as appropriate and adhere to the existing coding style. - -## License - -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. - -## Versioning - -Check the [CHANGELOG.md](CHANGELOG.md) for detailed release notes and version changes. - -## Acknowledgements - -- [Dear ImGui](https://github.com/ocornut/imgui) - The immediate mode GUI library -- [Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui) - .NET bindings for Dear ImGui -- [Silk.NET](https://github.com/dotnet/Silk.NET) - .NET bindings for OpenGL and windowing -- All contributors and the .NET community for their support - -## Support - -If you encounter any issues or have questions, please [open an issue](https://github.com/ktsu-dev/ImGuiApp/issues). diff --git a/ImGui.Popups/CompatibilitySuppressions.xml b/ImGui.Popups/CompatibilitySuppressions.xml deleted file mode 100644 index 9bd50a2..0000000 --- a/ImGui.Popups/CompatibilitySuppressions.xml +++ /dev/null @@ -1,52 +0,0 @@ -īģŋ - - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureGuardAttribute - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0001 - T:System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0001 - T:System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0001 - T:System.Runtime.CompilerServices.ParamCollectionAttribute - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0001 - T:System.Threading.Lock - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_UrlFormat:[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_UrlFormat(System.String):[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Popups.dll - lib/net9.0/ktsu.ImGui.Popups.dll - - \ No newline at end of file diff --git a/ImGui.Popups/DESCRIPTION.md b/ImGui.Popups/DESCRIPTION.md deleted file mode 100644 index 93ac7bd..0000000 --- a/ImGui.Popups/DESCRIPTION.md +++ /dev/null @@ -1 +0,0 @@ -A library for custom popups using ImGui.NET. diff --git a/ImGui.Popups/FilesystemBrowser.cs b/ImGui.Popups/FilesystemBrowser.cs deleted file mode 100644 index e3ce9e4..0000000 --- a/ImGui.Popups/FilesystemBrowser.cs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Numerics; -using System.Text.Json.Serialization; - -using Hexa.NET.ImGui; - -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; -using ktsu.Extensions; -using Microsoft.Extensions.FileSystemGlobbing; -using System.Text; - -/// -/// Partial class containing various ImGui popup implementations. -/// -public partial class ImGuiPopups -{ - /// - /// Defines the mode of operation for the filesystem browser. - /// - public enum FilesystemBrowserMode - { - /// - /// Browser operates in file/directory open mode, allowing selection of existing items. - /// - Open, - - /// - /// Browser operates in save mode, allowing selection of existing items or input of new filenames. - /// - Save - } - - /// - /// Defines the target type for the filesystem browser. - /// - public enum FilesystemBrowserTarget - { - /// - /// Target is a file. - /// - File, - - /// - /// Target is a directory. - /// - Directory - } - - /// - /// A class for displaying a filesystem browser popup window. - /// - public class FilesystemBrowser - { - /// - /// Gets or sets the mode of the browser (Open or Save). - /// - private FilesystemBrowserMode BrowserMode { get; set; } - - /// - /// Gets or sets the target type of the browser (File or Directory). - /// - private FilesystemBrowserTarget BrowserTarget { get; set; } - - /// - /// Action to invoke when a file is chosen. - /// - private Action OnChooseFile { get; set; } = (f) => { }; - - /// - /// Action to invoke when a directory is chosen. - /// - private Action OnChooseDirectory { get; set; } = (d) => { }; - - /// - /// Gets or sets the current directory being displayed. - /// - [JsonInclude] - private AbsoluteDirectoryPath CurrentDirectory { get; set; } = Environment.CurrentDirectory.As(); - - /// - /// Collection of current contents (files and directories) in the current directory. - /// - private Collection CurrentContents { get; set; } = []; - - /// - /// The currently selected item. - /// - private IAbsolutePath ChosenItem { get; set; } = AbsolutePath.Create(""); - - /// - /// Collection of logical drives available. - /// - private Collection Drives { get; set; } = []; - - /// - /// The glob pattern used for filtering files. - /// - private string Glob { get; set; } = "*"; - - /// - /// Matcher used for file globbing. - /// - private Matcher Matcher { get; set; } = new(); - - /// - /// The filename entered by the user. - /// - private FileName FileName { get; set; } = new(); - - /// - /// The modal instance for displaying the browser popup. - /// - private Modal Modal { get; } = new(); - - /// - /// The popup message for displaying alerts. - /// - private MessageOK PopupMessageOK { get; } = new(); - - /// - /// Opens the file open dialog with the specified title and callback. - /// - /// The title of the dialog. - /// Callback invoked when a file is chosen. - /// Glob pattern for filtering files. - public void FileOpen(string title, Action onChooseFile, string glob = "*") => FileOpen(title, onChooseFile, customSize: Vector2.Zero, glob); - - /// - /// Opens the file open dialog with the specified title, callback, and custom size. - /// - /// The title of the dialog. - /// Callback invoked when a file is chosen. - /// Custom size of the dialog. - /// Glob pattern for filtering files. - public void FileOpen(string title, Action onChooseFile, Vector2 customSize, string glob = "*") => File(title, FilesystemBrowserMode.Open, onChooseFile, customSize, glob); - - /// - /// Opens the file save dialog with the specified title and callback. - /// - /// The title of the dialog. - /// Callback invoked when a file is chosen. - /// Glob pattern for filtering files. - public void FileSave(string title, Action onChooseFile, string glob = "*") => FileSave(title, onChooseFile, customSize: Vector2.Zero, glob); - - /// - /// Opens the file save dialog with the specified title, callback, and custom size. - /// - /// The title of the dialog. - /// Callback invoked when a file is chosen. - /// Custom size of the dialog. - /// Glob pattern for filtering files. - public void FileSave(string title, Action onChooseFile, Vector2 customSize, string glob = "*") => File(title, FilesystemBrowserMode.Save, onChooseFile, customSize, glob); - - /// - /// Opens the filesystem browser popup with the specified parameters. - /// - /// The title of the popup. - /// The mode of the browser (Open or Save). - /// Callback for when a file is chosen. - /// Custom size of the popup. - /// Glob pattern for filtering files. - private void File(string title, FilesystemBrowserMode mode, Action onChooseFile, Vector2 customSize, string glob) => OpenPopup(title, mode, FilesystemBrowserTarget.File, onChooseFile, (d) => { }, customSize, glob); - - /// - /// Opens the directory chooser dialog with the specified title and callback. - /// - /// The title of the dialog. - /// Callback invoked when a directory is chosen. - public void ChooseDirectory(string title, Action onChooseDirectory) => ChooseDirectory(title, onChooseDirectory, customSize: Vector2.Zero); - - /// - /// Opens the directory chooser dialog with the specified title, callback, and custom size. - /// - /// The title of the dialog. - /// Callback invoked when a directory is chosen. - /// Custom size of the dialog. - public void ChooseDirectory(string title, Action onChooseDirectory, Vector2 customSize) => OpenPopup(title, FilesystemBrowserMode.Open, FilesystemBrowserTarget.Directory, (d) => { }, onChooseDirectory, customSize, "*"); - - /// - /// Opens the filesystem browser popup with the specified parameters. - /// - /// The title of the popup. - /// The mode of the browser (Open or Save). - /// The target type (File or Directory). - /// Callback for when a file is chosen. - /// Callback for when a directory is chosen. - /// Custom size of the popup. - /// Glob pattern for filtering files. - private void OpenPopup(string title, FilesystemBrowserMode mode, FilesystemBrowserTarget target, Action onChooseFile, Action onChooseDirectory, Vector2 customSize, string glob) - { - FileName = new(); - BrowserMode = mode; - BrowserTarget = target; - OnChooseFile = onChooseFile; - OnChooseDirectory = onChooseDirectory; - Glob = glob; - Matcher = new(); - Matcher.AddInclude(Glob); - Drives.Clear(); - Environment.GetLogicalDrives().ForEach(Drives.Add); - RefreshContents(); - Modal.Open(title, ShowContent, customSize); - } - - /// - /// Displays the content of the filesystem browser popup. - /// - private void ShowContent() - { - if (Drives.Count != 0) - { - if (ImGui.BeginCombo("##Drives", Drives[0])) - { - StringBuilder currentDriveStringBuilder = new(); - currentDriveStringBuilder.Append(CurrentDirectory.Split(Path.VolumeSeparatorChar).Current); - currentDriveStringBuilder.Append(Path.VolumeSeparatorChar); - currentDriveStringBuilder.Append(Path.DirectorySeparatorChar); - string currentDrive = currentDriveStringBuilder.ToString(); - - foreach (string drive in Drives) - { - if (ImGui.Selectable(drive, drive == currentDrive)) - { - CurrentDirectory = drive.As(); - RefreshContents(); - } - } - - ImGui.EndCombo(); - } - } - - ImGui.TextUnformatted($"{CurrentDirectory}{Path.DirectorySeparatorChar}{Glob}"); - if (ImGui.BeginChild("FilesystemBrowser", new(500, 400), ImGuiChildFlags.None)) - { - if (ImGui.BeginTable(nameof(FilesystemBrowser), 1, ImGuiTableFlags.Borders)) - { - ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch, 40); - //ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.None, 3); - //ImGui.TableSetupColumn("Modified", ImGuiTableColumnFlags.None, 3); - ImGui.TableHeadersRow(); - - ImGuiSelectableFlags flags = ImGuiSelectableFlags.SpanAllColumns | ImGuiSelectableFlags.AllowDoubleClick | ImGuiSelectableFlags.NoAutoClosePopups; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - if (ImGui.Selectable("..", false, flags)) - { - if (ImGui.IsMouseDoubleClicked(0)) - { - string? newPath = Path.GetDirectoryName(CurrentDirectory.WeakString.Trim(Path.DirectorySeparatorChar)); - if (newPath is not null) - { - CurrentDirectory = newPath.As(); - RefreshContents(); - } - } - } - - foreach (IAbsolutePath? path in CurrentContents.OrderBy(p => p is not AbsoluteDirectoryPath).ThenBy(p => p).ToCollection()) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - AbsoluteDirectoryPath? directory = path as AbsoluteDirectoryPath; - AbsoluteFilePath? file = path as AbsoluteFilePath; - string displayPath = directory?.WeakString ?? file?.WeakString ?? string.Empty; - displayPath = displayPath.RemovePrefix(CurrentDirectory.WeakString).Trim(Path.DirectorySeparatorChar); - - if (directory is not null) - { - displayPath += Path.DirectorySeparatorChar; - } - - if (ImGui.Selectable(displayPath, ChosenItem == path, flags)) - { - if (directory is not null) - { - ChosenItem = directory; - if (ImGui.IsMouseDoubleClicked(0)) - { - CurrentDirectory = directory; - RefreshContents(); - } - } - else if (file is not null) - { - ChosenItem = file; - FileName = file.FileName; - if (ImGui.IsMouseDoubleClicked(0)) - { - ChooseItem(); - } - } - } - } - - ImGui.EndTable(); - } - } - - ImGui.EndChild(); - - if (BrowserMode == FilesystemBrowserMode.Save) - { - string fileName = FileName; - ImGui.InputText("##SaveAs", ref fileName, 256); - FileName = fileName.As(); - } - - string confirmText = BrowserMode switch - { - FilesystemBrowserMode.Open => "Open", - FilesystemBrowserMode.Save => "Save", - _ => "Choose" - }; - if (ImGui.Button(confirmText)) - { - ChooseItem(); - } - - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - { - ImGui.CloseCurrentPopup(); - } - - PopupMessageOK.ShowIfOpen(); - } - - /// - /// Handles the selection of an item (file or directory) based on the current target. - /// - private void ChooseItem() - { - if (BrowserTarget == FilesystemBrowserTarget.File) - { - AbsoluteFilePath chosenFile = CurrentDirectory / FileName; - if (!Matcher.Match(FileName).HasMatches) - { - PopupMessageOK.Open("Invalid File Name", "The file name does not match the glob pattern."); - return; - } - - OnChooseFile(Path.GetFullPath(chosenFile).As()); - } - else if (BrowserTarget == FilesystemBrowserTarget.Directory && ChosenItem is AbsoluteDirectoryPath directory) - { - OnChooseDirectory(Path.GetFullPath(directory).As()); - } - - ImGui.CloseCurrentPopup(); - } - - /// - /// Refreshes the contents of the current directory based on the target and glob pattern. - /// - private void RefreshContents() - { - ChosenItem = AbsolutePath.Create(""); - CurrentContents.Clear(); - CurrentDirectory.GetContents().ForEach(p => - { - if (BrowserTarget == FilesystemBrowserTarget.File || (BrowserTarget == FilesystemBrowserTarget.Directory && p is AbsoluteDirectoryPath)) - { - if (p is not AbsolutePath absolutePath) - { - throw new InvalidOperationException("Path is not an absolute path."); - } - - if (absolutePath.IsDirectory || Matcher.Match(Path.GetFileName(absolutePath.WeakString)).HasMatches) - { - CurrentContents.Add(absolutePath); - } - } - }); - } - - /// - /// Shows the modal popup if it is open. - /// - /// True if the modal is open; otherwise, false. - public bool ShowIfOpen() => Modal.ShowIfOpen(); - } -} diff --git a/ImGui.Popups/ImGui.Popups.csproj b/ImGui.Popups/ImGui.Popups.csproj deleted file mode 100644 index 7b521c9..0000000 --- a/ImGui.Popups/ImGui.Popups.csproj +++ /dev/null @@ -1,18 +0,0 @@ -īģŋ - - - - - net8.0;net9.0 - - - - - - - - - - - - diff --git a/ImGui.Popups/Input.cs b/ImGui.Popups/Input.cs deleted file mode 100644 index f75a769..0000000 --- a/ImGui.Popups/Input.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System.Numerics; - -using Hexa.NET.ImGui; -using ktsu.CaseConverter; - -public partial class ImGuiPopups -{ - /// - /// Base class for a popup input window. - /// - /// The type of the input value. - /// The type of the derived class for CRTP. - public abstract class Input where TDerived : Input, new() - { - private TInput? cachedValue; - private Action OnConfirm { get; set; } = null!; - private string Label { get; set; } = string.Empty; - /// - /// Gets the modal instance used to display the popup input window. - /// - protected Modal Modal { get; } = new(); - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// The default value of the input field. - /// A callback to handle the new input value. - public void Open(string title, string label, TInput defaultValue, Action onConfirm) => Open(title, label, defaultValue, onConfirm, customSize: Vector2.Zero); - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// The default value of the input field. - /// A callback to handle the new input value. - /// Custom size of the popup. - public void Open(string title, string label, TInput defaultValue, Action onConfirm, Vector2 customSize) - { - Label = label; - OnConfirm = onConfirm; - cachedValue = defaultValue; - Modal.Open(title, ShowContent, customSize); - } - - /// - /// Show the content of the popup. - /// - private void ShowContent() - { - if (cachedValue is not null) - { - ImGui.TextUnformatted(Label); - ImGui.NewLine(); - - if (!Modal.WasOpen && !ImGui.IsItemFocused()) - { - ImGui.SetKeyboardFocusHere(); - } - - if (ShowEdit(ref cachedValue)) - { - OnConfirm(cachedValue); - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (ImGui.Button($"OK###{Modal.Title.ToSnakeCase()}_OK")) - { - OnConfirm(cachedValue); - ImGui.CloseCurrentPopup(); - } - } - } - - /// - /// Show the input field for the derived class. - /// - /// The input value. - /// True if the input field is changed. - protected abstract bool ShowEdit(ref TInput value); - - /// - /// Show the modal if it is open. - /// - /// True if the modal is open. - public bool ShowIfOpen() => Modal.ShowIfOpen(); - } - - /// - /// A popup input window for strings. - /// - public class InputString : Input - { - /// - /// Show the input field for strings. - /// - /// The input value. - /// True if Enter is pressed. - protected override bool ShowEdit(ref string value) => ImGui.InputText($"###{Modal.Title.ToSnakeCase()}_INPUT", ref value, 100, ImGuiInputTextFlags.EnterReturnsTrue); - } - - /// - /// A popup input window for integers. - /// - public class InputInt : Input - { - /// - /// Show the input field for integers. - /// - /// The input value. - /// False - protected override bool ShowEdit(ref int value) - { - ImGui.InputInt($"###{Modal.Title.ToSnakeCase()}_INPUT", ref value); - return false; - } - } - - /// - /// A popup input window for floats. - /// - public class InputFloat : Input - { - /// - /// Show the input field for floats. - /// - /// The input value. - /// False - protected override bool ShowEdit(ref float value) - { - ImGui.InputFloat($"###{Modal.Title.ToSnakeCase()}_INPUT", ref value); - return false; - } - } -} diff --git a/ImGui.Popups/MessageOK.cs b/ImGui.Popups/MessageOK.cs deleted file mode 100644 index 87949fa..0000000 --- a/ImGui.Popups/MessageOK.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System.Numerics; - -public partial class ImGuiPopups -{ - /// - /// A class for displaying a prompt popup window. - /// - public class MessageOK : Prompt - { - /// - /// Open the popup and set the title and message. - /// - /// The title of the popup window. - /// The message to show. - public void Open(string title, string message) => Open(title, message, customSize: Vector2.Zero); - /// - /// Open the popup and set the title and message. - /// - /// The title of the popup window. - /// The message to show. - /// Custom size of the popup. - public void Open(string title, string message, Vector2 customSize) => Open(title, message, PromptTextLayoutType.Unformatted, customSize); - /// - /// Open the popup and set the title and message. - /// - /// The title of the popup window. - /// The message to show. - /// Which text layout method should be used. - /// Custom size of the popup. - public void Open(string title, string message, PromptTextLayoutType textLayoutType, Vector2 size) => Open(title, message, new() { { "OK", null } }, textLayoutType, size); - } -} diff --git a/ImGui.Popups/Modal.cs b/ImGui.Popups/Modal.cs deleted file mode 100644 index 8b03a64..0000000 --- a/ImGui.Popups/Modal.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.CaseConverter; - -public static partial class ImGuiPopups -{ - /// - /// Base class for a modal window. - /// - public class Modal - { - /// - /// Gets or sets the title of the modal window. - /// - internal string Title { get; set; } = string.Empty; - - /// - /// Determines whether the modal should open. - /// - private bool ShouldOpen { get; set; } - - /// - /// The delegate to invoke to show the content of the modal. - /// - private Action OnShowContent { get; set; } = () => { }; - - /// - /// Gets or sets the custom size of the popup. - /// - private Vector2 CustomSize { get; set; } = Vector2.Zero; - - /// - /// Gets a value indicating whether the modal was open. - /// - public bool WasOpen { get; private set; } - - /// - /// Gets the id of the modal window. - /// - /// The id of the modal window. - private string Name => $"{Title}###Modal_{Title.ToSnakeCase()}"; - - /// - /// Opens the modal and sets the title. - /// - /// The title of the modal window. - /// The delegate to invoke to show the content of the modal. - public void Open(string title, Action onShowContent) => Open(title, onShowContent, customSize: Vector2.Zero); - - /// - /// Opens the modal and sets the title with a custom size. - /// - /// The title of the modal window. - /// The delegate to invoke to show the content of the modal. - /// Custom size of the popup. - public void Open(string title, Action onShowContent, Vector2 customSize) - { - Title = title; - OnShowContent = onShowContent; - ShouldOpen = true; - CustomSize = customSize; - } - - /// - /// Shows the modal if it is open. - /// - /// True if the modal is open. - public bool ShowIfOpen() - { - if (ShouldOpen) - { - ShouldOpen = false; - ImGui.OpenPopup(Name); - } - - bool result = ImGui.IsPopupOpen(Name); - if (CustomSize != Vector2.Zero) - { - ImGui.SetNextWindowSize(CustomSize); - } - - if (ImGui.BeginPopupModal(Name, ref result, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings)) - { - OnShowContent(); - - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - WasOpen = result; - return result; - } - } -} diff --git a/ImGui.Popups/Prompt.cs b/ImGui.Popups/Prompt.cs deleted file mode 100644 index 35c2aa9..0000000 --- a/ImGui.Popups/Prompt.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Numerics; - -using Hexa.NET.ImGui; - -/// -/// Contains classes for displaying various popup windows using ImGui. -/// -public static partial class ImGuiPopups -{ - /// - /// Defines the layout type for prompt text. - /// - public enum PromptTextLayoutType - { - /// - /// The text is displayed without any formatting. - /// - Unformatted, - - /// - /// The text is wrapped based on the popup's size. - /// - Wrapped - } - - /// - /// A class for displaying a prompt popup window. - /// - public class Prompt - { - /// - /// Gets the underlying modal instance. - /// - private Modal Modal { get; } = new(); - - /// - /// Gets or sets the label of the prompt. - /// - private string Label { get; set; } = string.Empty; - - /// - /// Gets or sets the dictionary of button labels and their corresponding actions. - /// - private Dictionary Buttons { get; set; } = []; - - /// - /// Gets or sets the text layout type for the prompt. - /// - private PromptTextLayoutType TextLayoutType { get; set; } - - /// - /// Opens the prompt popup with the specified title, label, and buttons. - /// - /// The title of the popup window. - /// The label of the input field. - /// The names and actions of the buttons. - public virtual void Open(string title, string label, Dictionary buttons) - => Open(title, label, buttons, customSize: Vector2.Zero); - - /// - /// Opens the prompt popup with the specified title, label, buttons, and custom size. - /// - /// The title of the popup window. - /// The label of the input field. - /// The names and actions of the buttons. - /// Custom size of the popup. - public virtual void Open(string title, string label, Dictionary buttons, Vector2 customSize) - => Open(title, label, buttons, textLayoutType: PromptTextLayoutType.Unformatted, customSize); - - /// - /// Opens the prompt popup with the specified parameters. - /// - /// The title of the popup window. - /// The label of the input field. - /// The names and actions of the buttons. - /// The layout type for the prompt text. - /// Custom size of the popup. - public void Open(string title, string label, Dictionary buttons, PromptTextLayoutType textLayoutType, Vector2 size) - { - // Wrapping text without a custom size will result in an incorrectly sized - // popup as the text will wrap based on the popup and the popup will size - // based on the text. - Debug.Assert((textLayoutType == PromptTextLayoutType.Unformatted) || (size != Vector2.Zero)); - - Label = label; - Buttons = buttons; - TextLayoutType = textLayoutType; - Modal.Open(title, ShowContent, size); - } - - /// - /// Displays the content of the prompt popup based on the text layout type. - /// - private void ShowContent() - { - switch (TextLayoutType) - { - case PromptTextLayoutType.Unformatted: - ImGui.TextUnformatted(Label); - break; - - case PromptTextLayoutType.Wrapped: - ImGui.TextWrapped(Label); - break; - - default: - throw new NotImplementedException(); - } - - ImGui.NewLine(); - - foreach ((string text, Action? action) in Buttons) - { - if (ImGui.Button(text)) - { - action?.Invoke(); - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - } - } - - /// - /// Displays the modal if it is open. - /// - /// True if the modal is open; otherwise, false. - public bool ShowIfOpen() => Modal.ShowIfOpen(); - } -} diff --git a/ImGui.Popups/README.md b/ImGui.Popups/README.md deleted file mode 100644 index 2cfb5cd..0000000 --- a/ImGui.Popups/README.md +++ /dev/null @@ -1,251 +0,0 @@ -# ImGuiPopups - -[![Version](https://img.shields.io/badge/version-1.3.5-blue.svg)](VERSION.md) -[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md) - -A comprehensive library for custom popup windows and modal dialogs using ImGui.NET, providing a rich set of UI components for interactive applications. - -## Features - -### đŸĒŸ Modal Windows -- **Modal**: Base modal window with customizable content and size -- **MessageOK**: Simple message dialog with OK button -- **Prompt**: Customizable prompt with multiple button options - -### 📝 Input Components -- **InputString**: Text input popup with validation -- **InputInt**: Integer input popup with numeric validation -- **InputFloat**: Floating-point input popup with numeric validation - -### 🔍 Selection Components -- **SearchableList**: Searchable dropdown list with filtering capabilities -- **FilesystemBrowser**: Advanced file/directory browser with: - - Open and Save modes - - File and Directory targeting - - Pattern filtering support - - Navigation breadcrumbs - -### ✨ Key Features -- **Responsive Design**: All popups adapt to content and custom sizing -- **Keyboard Navigation**: Full keyboard support with proper focus management -- **Validation**: Built-in input validation and error handling -- **Customizable**: Flexible styling and layout options -- **Type-Safe**: Generic components with strong typing - -## Installation - -### Package Manager Console -```powershell -Install-Package ktsu.ImGuiPopups -``` - -### .NET CLI -```bash -dotnet add package ktsu.ImGuiPopups -``` - -### PackageReference -```xml - -``` - -## Quick Start - -```csharp -using ktsu.ImGuiPopups; - -// Create popup instances (typically as class members) -private static readonly ImGuiPopups.MessageOK messageOK = new(); -private static readonly ImGuiPopups.InputString inputString = new(); -private static readonly ImGuiPopups.SearchableList searchableList = new(); - -// In your ImGui render loop -private void OnRender() -{ - // Show a simple message - if (ImGui.Button("Show Message")) - { - messageOK.Open("Information", "Hello, World!"); - } - - // Get text input from user - if (ImGui.Button("Get Input")) - { - inputString.Open("Enter Name", "Name:", "Default Name", - result => Console.WriteLine($"User entered: {result}")); - } - - // Show searchable selection - if (ImGui.Button("Select Item")) - { - var items = new[] { "Apple", "Banana", "Cherry", "Date" }; - searchableList.Open("Select Fruit", "Choose:", items, null, - item => item, // Text converter - selected => Console.WriteLine($"Selected: {selected}"), - Vector2.Zero); - } - - // Render all popups (call this once per frame) - messageOK.ShowIfOpen(); - inputString.ShowIfOpen(); - searchableList.ShowIfOpen(); -} -``` - -## Component Documentation - -### MessageOK -Simple message dialog with an OK button. - -```csharp -var messageOK = new ImGuiPopups.MessageOK(); -messageOK.Open("Title", "Your message here"); -``` - -### Input Components -Get validated input from users: - -```csharp -// String input -var inputString = new ImGuiPopups.InputString(); -inputString.Open("Enter Text", "Label:", "default", result => HandleString(result)); - -// Integer input -var inputInt = new ImGuiPopups.InputInt(); -inputInt.Open("Enter Number", "Value:", 42, result => HandleInt(result)); - -// Float input -var inputFloat = new ImGuiPopups.InputFloat(); -inputFloat.Open("Enter Float", "Value:", 3.14f, result => HandleFloat(result)); -``` - -### SearchableList -Searchable selection from a list of items: - -```csharp -var searchableList = new ImGuiPopups.SearchableList(); -searchableList.Open( - title: "Select Item", - label: "Choose an item:", - items: myItemList, - defaultItem: null, - getText: item => item.DisplayName, // How to display items - onConfirm: selected => HandleSelection(selected), - customSize: new Vector2(400, 300) -); -``` - -### FilesystemBrowser -Advanced file and directory browser: - -```csharp -var browser = new ImGuiPopups.FilesystemBrowser(); - -// Open file -browser.Open( - title: "Open File", - mode: FilesystemBrowserMode.Open, - target: FilesystemBrowserTarget.File, - startPath: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - onConfirm: path => OpenFile(path), - patterns: new[] { "*.txt", "*.md" } // Optional file filters -); - -// Save file -browser.Open( - title: "Save File", - mode: FilesystemBrowserMode.Save, - target: FilesystemBrowserTarget.File, - startPath: currentDirectory, - onConfirm: path => SaveFile(path) -); -``` - -### Custom Modal -Create custom modal dialogs: - -```csharp -var customModal = new ImGuiPopups.Modal(); -customModal.Open("Custom Dialog", () => { - ImGui.Text("Custom content here"); - if (ImGui.Button("Close")) - { - ImGui.CloseCurrentPopup(); - } -}, new Vector2(300, 200)); -``` - -## Advanced Usage - -### Custom Sizing -All popups support custom sizing: - -```csharp -// Fixed size -popup.Open("Title", "Content", new Vector2(400, 300)); - -// Auto-size (Vector2.Zero) -popup.Open("Title", "Content", Vector2.Zero); -``` - -### Text Layout Options -Prompts support different text layout modes: - -```csharp -var prompt = new ImGuiPopups.Prompt(); -prompt.Open("Title", "Long message text here...", - buttons: new() { { "OK", null }, { "Cancel", null } }, - textLayoutType: PromptTextLayoutType.Wrapped, // or Unformatted - size: new Vector2(400, 200) -); -``` - -### Validation and Error Handling -Input components provide built-in validation: - -```csharp -inputInt.Open("Enter Age", "Age (1-120):", 25, result => { - if (result < 1 || result > 120) - { - messageOK.Open("Error", "Age must be between 1 and 120"); - return; - } - ProcessAge(result); -}); -``` - -## Demo Application - -The repository includes a comprehensive demo application showcasing all components: - -```bash -git clone https://github.com/ktsu-dev/ImGuiPopups.git -cd ImGuiPopups -dotnet run --project ImGuiPopupsDemo -``` - -## Dependencies - -- [Hexa.NET.ImGui](https://www.nuget.org/packages/Hexa.NET.ImGui/) - ImGui.NET bindings -- [ktsu.Extensions](https://www.nuget.org/packages/ktsu.Extensions/) - Utility extensions -- [ktsu.CaseConverter](https://www.nuget.org/packages/ktsu.CaseConverter/) - String case conversion -- [ktsu.ScopedAction](https://www.nuget.org/packages/ktsu.ScopedAction/) - RAII-style actions -- [ktsu.StrongPaths](https://www.nuget.org/packages/ktsu.StrongPaths/) - Type-safe path handling -- [ktsu.TextFilter](https://www.nuget.org/packages/ktsu.TextFilter/) - Text filtering utilities -- [Microsoft.Extensions.FileSystemGlobbing](https://www.nuget.org/packages/Microsoft.Extensions.FileSystemGlobbing/) - File pattern matching - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. - -## License - -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. - -## Changelog - -See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes. - ---- - -**ktsu.dev** - Building tools for developers diff --git a/ImGui.Popups/SearchableList.cs b/ImGui.Popups/SearchableList.cs deleted file mode 100644 index b539f02..0000000 --- a/ImGui.Popups/SearchableList.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Popups; - -using System; -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.CaseConverter; -using ktsu.TextFilter; - -public partial class ImGuiPopups -{ - /// - /// A popup window to allow the user to search and select an item from a list - /// - /// The type of the list elements - public class SearchableList where TItem : class - { - private TItem? cachedValue; - private TItem? selectedItem; - private string searchTerm = string.Empty; - private Action OnConfirm { get; set; } = null!; - private Func? GetText { get; set; } - private string Label { get; set; } = string.Empty; - private IEnumerable Items { get; set; } = []; - private Modal Modal { get; } = new(); - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// /// The items to select from. - /// The default value of the input field. - /// A delegate to get the text representation of an item. - /// A callback to handle the new input value. - /// Custom size of the popup. - public void Open(string title, string label, IEnumerable items, TItem? defaultItem, Func? getText, Action onConfirm, Vector2 customSize) - { - searchTerm = string.Empty; - Label = label; - OnConfirm = onConfirm; - GetText = getText; - cachedValue = defaultItem; - Items = items; - Modal.Open(title, ShowContent, customSize); - } - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// The items to select from. - /// The default value of the input field. - /// A delegate to get the text representation of an item. - /// A callback to handle the new input value. - public void Open(string title, string label, IEnumerable items, TItem? defaultItem, Func? getText, Action onConfirm) => Open(title, label, items, defaultItem, getText, onConfirm, Vector2.Zero); - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// The items to select from. - /// A callback to handle the new input value. - public void Open(string title, string label, IEnumerable items, Action onConfirm) => Open(title, label, items, null, null, onConfirm); - - /// - /// Open the popup and set the title, label, and default value. - /// - /// The title of the popup window. - /// The label of the input field. - /// The items to select from. - /// A delegate to get the text representation of an item. - /// A callback to handle the new input value. - public void Open(string title, string label, IEnumerable items, Func getText, Action onConfirm) => Open(title, label, items, null, getText, onConfirm); - - /// - /// Show the content of the popup. - /// - private void ShowContent() - { - ImGui.TextUnformatted(Label); - ImGui.NewLine(); - if (!Modal.WasOpen && !ImGui.IsItemFocused()) - { - ImGui.SetKeyboardFocusHere(); - } - - if (ImGui.InputText("##Search", ref searchTerm, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - TItem? confirmedItem = cachedValue ?? selectedItem; - if (confirmedItem is not null) - { - OnConfirm(confirmedItem); - ImGui.CloseCurrentPopup(); - } - } - - Dictionary itemLookup = Items.Select(item => (item, itemString: item.ToString() ?? string.Empty)) - .Where(x => !string.IsNullOrEmpty(x.itemString)) - .DistinctBy(x => x.itemString) - .ToDictionary(x => x.itemString, x => x.item); - - IEnumerable sortedStrings = TextFilter.Rank(itemLookup.Keys, searchTerm); - - if (ImGui.BeginListBox("##List")) - { - selectedItem = null; - foreach (string itemString in sortedStrings) - { - if (!itemLookup.TryGetValue(itemString, out TItem? item)) - { - continue; - } - - //if nothing has been explicitly selected, select the first item which will be the best match - if (selectedItem is null && cachedValue is null) - { - selectedItem = item; - } - - string displayText = GetText?.Invoke(item) ?? item.ToString() ?? string.Empty; - - if (ImGui.Selectable(displayText, item == (cachedValue ?? selectedItem))) - { - cachedValue = item; - } - } - - ImGui.EndListBox(); - } - - if (ImGui.Button($"OK###{Modal.Title.ToSnakeCase()}_OK")) - { - TItem? confirmedItem = cachedValue ?? selectedItem; - if (confirmedItem is not null) - { - OnConfirm(confirmedItem); - ImGui.CloseCurrentPopup(); - } - } - - ImGui.SameLine(); - if (ImGui.Button($"Cancel###{Modal.Title.ToSnakeCase()}_Cancel")) - { - ImGui.CloseCurrentPopup(); - } - } - - /// - /// Show the modal if it is open. - /// - /// True if the modal is open. - public bool ShowIfOpen() => Modal.ShowIfOpen(); - } -} diff --git a/ImGui.Styler/Alignment.cs b/ImGui.Styler/Alignment.cs deleted file mode 100644 index 869518a..0000000 --- a/ImGui.Styler/Alignment.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ScopedAction; - -/// -/// Provides methods for aligning content within a container. -/// -public static class Alignment -{ - /// - /// Centers the content within the available content region. - /// - /// The size of the content to be centered. - public class Center(Vector2 contentSize) : CenterWithin(contentSize, new Vector2(ImGui.GetContentRegionAvail().X, contentSize.Y)) - { - } - - /// - /// Centers the content within a specified container size. - /// - public class CenterWithin : ScopedAction - { - /// - /// Initializes a new instance of the class. - /// - /// The size of the content to be centered. - /// The size of the container within which the content will be centered. - public CenterWithin(Vector2 contentSize, Vector2 containerSize) - { - // We need to manipulate the cursor a lot to support the layout of this widget and - // integrate with the layout methods of ImGui (eg. SameLine). Because contentDrawDelegate - // is called after the Dummy() it means that CursorPosPrevLine is set to an unexpected value - // so we "abuse" setting the cursor and calling NewLine to force CursorPosPrevLine to be what we need. - - // - Move the cursor to the top left of the content - // - Move the cursor to the top right of the container - // - Make a dummy with the height of the container so that the cursor will advance to - // a new line after the container size, while support ImGui.NewLine() to work as expected - Vector2 cursorContainerTopLeft = ImGui.GetCursorScreenPos(); - Vector2 clippedsize = new(Math.Min(contentSize.X, containerSize.X), Math.Min(contentSize.Y, containerSize.Y)); - Vector2 offset = (containerSize - clippedsize) / 2; - Vector2 cursorContentStart = cursorContainerTopLeft + offset; - ImGui.SetCursorScreenPos(cursorContentStart); - - OnClose = () => - { - ImGui.SetCursorScreenPos(cursorContainerTopLeft + new Vector2(containerSize.X, 0f)); - ImGui.Dummy(new Vector2(0, containerSize.Y)); - }; - } - } -} diff --git a/ImGui.Styler/Button.cs b/ImGui.Styler/Button.cs deleted file mode 100644 index eb14ce3..0000000 --- a/ImGui.Styler/Button.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Numerics; - -using Hexa.NET.ImGui; - -/// -/// Provides functionality for creating and aligning buttons in ImGui. -/// -public static class Button -{ - /// - /// Contains methods for aligning buttons. - /// - public static class Alignment - { - /// - /// Aligns the button text to the left. - /// - /// A scoped button alignment with left alignment. - public static ScopedButtonAlignment Left() => new(new(0f, 0.5f)); - - /// - /// Aligns the button text to the center. - /// - /// A scoped button alignment with center alignment. - public static ScopedButtonAlignment Center() => new(new(0.5f, 0.5f)); - - /// - /// Represents a scoped button alignment. - /// - /// The alignment vector. - public class ScopedButtonAlignment(Vector2 vector) : ScopedStyleVar(ImGuiStyleVar.ButtonTextAlign, vector) - { - } - } -} diff --git a/ImGui.Styler/Color.cs b/ImGui.Styler/Color.cs deleted file mode 100644 index 6c95655..0000000 --- a/ImGui.Styler/Color.cs +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Globalization; -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ThemeProvider; - -/// -/// Provides methods for creating and manipulating colors in ImGui. -/// -public static class Color -{ - /// - /// Represents the optimal text contrast ratio for accessibility. - /// - public const float OptimalTextContrastRatio = 4.5f; - - #region Color Creation Methods - - /// - /// Converts a hexadecimal color string to an object. - /// - /// The hexadecimal color string in the format #RRGGBB or #RRGGBBAA. - /// An object representing the color. - /// Thrown when the is null. - /// Thrown when the is not in the correct format. - public static ImColor FromHex(string hex) - { - ArgumentNullException.ThrowIfNull(hex, nameof(hex)); - - if (hex.StartsWith('#')) - { - hex = hex[1..]; - } - - if (hex.Length == 6) - { - hex += "FF"; - } - - if (hex.Length != 8) - { - throw new ArgumentException("Hex color must be in the format #RRGGBB or #RRGGBBAA", nameof(hex)); - } - - byte r = byte.Parse(hex.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - byte g = byte.Parse(hex.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - byte b = byte.Parse(hex.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - byte a = byte.Parse(hex.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - - return FromRGBA(r, g, b, a); - } - - /// - /// Creates an object from RGB byte values. - /// - /// The red component value (0-255). - /// The green component value (0-255). - /// The blue component value (0-255). - /// An object representing the color. - public static ImColor FromRGB(byte r, byte g, byte b) => new() - { - Value = new Vector4(r / 255f, g / 255f, b / 255f, 1f) - }; - - /// - /// Creates an object from RGBA byte values. - /// - /// The red component value (0-255). - /// The green component value (0-255). - /// The blue component value (0-255). - /// The alpha component value (0-255). - /// An object representing the color. - public static ImColor FromRGBA(byte r, byte g, byte b, byte a) => new() - { - Value = new Vector4(r / 255f, g / 255f, b / 255f, a / 255f) - }; - - /// - /// Creates an object from RGB float values. - /// - /// The red component value (0-1). - /// The green component value (0-1). - /// The blue component value (0-1). - /// An object representing the color. - public static ImColor FromRGB(float r, float g, float b) => new() - { - Value = new Vector4(r, g, b, 1f) - }; - - /// - /// Creates an object from RGBA float values. - /// - /// The red component value (0-1). - /// The green component value (0-1). - /// The blue component value (0-1). - /// The alpha component value (0-1). - /// An object representing the color. - public static ImColor FromRGBA(float r, float g, float b, float a) => new() - { - Value = new Vector4(r, g, b, a) - }; - - /// - /// Creates an object from a . - /// - /// The vector containing RGB values. - /// An object representing the color. - public static ImColor FromVector(Vector3 vector) => new() - { - Value = new Vector4(vector.X, vector.Y, vector.Z, 1f) - }; - - /// - /// Creates an object from a . - /// - /// The vector containing RGBA values. - /// An object representing the color. - public static ImColor FromVector(Vector4 vector) => new() - { - Value = vector - }; - - /// - /// Creates an object from HSL values. - /// - /// The vector containing HSL values. - /// An object representing the color. - public static ImColor FromHSL(Vector3 vector) => FromHSLA(vector.X, vector.Y, vector.Z, 1); - - /// - /// Creates an object from HSLA values. - /// - /// The vector containing HSLA values. - /// An object representing the color. - public static ImColor FromHSLA(Vector4 vector) => FromHSLA(vector.X, vector.Y, vector.Z, vector.W); - - /// - /// Creates an object from HSL values. - /// - /// The hue component value (0-1). - /// The saturation component value (0-1). - /// The lightness component value (0-1). - /// An object representing the color. - public static ImColor FromHSL(float h, float s, float l) => FromHSLA(h, s, l, 1); - - /// - /// Creates an object from HSLA values. - /// - /// The hue component value (0-1). - /// The saturation component value (0-1). - /// The lightness component value (0-1). - /// The alpha component value (0-1). - /// An object representing the color. - public static ImColor FromHSLA(float h, float s, float l, float a) - { - float r, g, b; - - if (s == 0) - { - r = g = b = l; - } - else - { - float q = l < 0.5f ? l * (1f + s) : l + s - (l * s); - float p = (2f * l) - q; - r = HueToRGB(p, q, h + (1f / 3f)); - g = HueToRGB(p, q, h); - b = HueToRGB(p, q, h - (1f / 3f)); - } - - return FromRGBA(r, g, b, a); - } - - /// - /// Converts a PerceptualColor from ThemeProvider to an ImColor. - /// - /// The PerceptualColor to convert. - /// An ImColor representing the same color. - public static ImColor FromPerceptualColor(PerceptualColor color) - { - RgbColor rgb = color.RgbValue; - return FromRGBA(rgb.R, rgb.G, rgb.B, 1f); - } - - #endregion - - #region Private Helper Methods - - /// - /// Converts a hue to an RGB component. - /// - /// The first parameter for the conversion. - /// The second parameter for the conversion. - /// The hue value. - /// The RGB component value. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Clarity over brevity")] - private static float HueToRGB(float p, float q, float t) - { - if (t < 0) - { - t += 1; - } - - if (t > 1) - { - t -= 1; - } - - if (t < 1f / 6f) - { - return p + ((q - p) * 6f * t); - } - - if (t < 1f / 2f) - { - return q; - } - - if (t < 2f / 3f) - { - return p + ((q - p) * ((2f / 3f) - t) * 6f); - } - - return p; - } - - /// - /// Gets a semantic color from the current theme, or a fallback color if no theme is applied. - /// This should only be used for semantic UI meanings. - /// - /// The semantic meaning of the color. - /// The priority level for the color. - /// The fallback color to use if no theme is applied. - /// An ImColor from the current theme or the fallback color. - private static ImColor GetSemanticColor(SemanticMeaning meaning, Priority priority, ImColor fallbackColor) - { - // Check if a theme is currently applied - if (Theme.CurrentTheme is not null) - { - try - { - // Create a semantic color request - SemanticColorRequest request = new(meaning, priority); - - // Use SemanticColorMapper to get the color from the current theme - IReadOnlyDictionary colorMapping = SemanticColorMapper.MapColors([request], Theme.CurrentTheme.CreateInstance()); - - if (colorMapping.TryGetValue(request, out PerceptualColor perceptualColor)) - { - return FromPerceptualColor(perceptualColor); - } - } - catch (ArgumentException) - { - // Invalid arguments for theme mapping - } - catch (InvalidOperationException) - { - // Theme operation failed - } - } - - // Fall back to hardcoded color if no theme is applied or mapping fails - return fallbackColor; - } - - /// - /// Gets a color from the current theme that is closest to the desired default color, - /// or returns the fallback color if no theme is applied. - /// This preserves the intended hue while adapting to the theme's color scheme. - /// - /// The default hardcoded color to find a close match for. - /// An ImColor that's close to the fallback color within the current theme, or the fallback color itself. - private static ImColor GetThemeColor(ImColor fallbackColor) - { - // Check if a theme is currently applied and get its complete palette - IReadOnlyDictionary? completePalette = Theme.GetCurrentThemeCompletePalette(); - if (completePalette is not null) - { - try - { - // Convert the fallback color to PerceptualColor for comparison - RgbColor fallbackRgb = new(fallbackColor.Value.X, fallbackColor.Value.Y, fallbackColor.Value.Z); - PerceptualColor targetColor = new(fallbackRgb); - - PerceptualColor? closestColor = null; - float closestDistance = float.MaxValue; - - // Search through the complete palette to find the closest match - // This is much more efficient than nested loops through semantic mappings - foreach (PerceptualColor color in completePalette.Values) - { - float distance = targetColor.SemanticDistanceTo(color); - if (distance < closestDistance) - { - closestDistance = distance; - closestColor = color; - } - } - - // If we found a reasonably close color, use it - if (closestColor.HasValue && closestDistance < 0.3f) // Reasonable similarity threshold - { - return FromPerceptualColor(closestColor.Value); - } - } - catch (ArgumentException) - { - // Invalid arguments for theme color matching - } - catch (InvalidOperationException) - { - // Theme operation failed - } - } - - // Fall back to hardcoded color if no theme is applied or no close match found - return fallbackColor; - } - - #endregion - - /// - /// Comprehensive color palette with organized categories. - /// Semantic colors are sourced from the current theme's semantic meanings. - /// Other colors try to find close matches in the theme while preserving intended hues. - /// - public static class Palette - { - /// - /// Basic primary and secondary colors. - /// These try to find close colors in the current theme while preserving the intended hue. - /// - public static class Basic - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor Red => GetThemeColor(FromHex("#ff4a49")); - public static ImColor Green => GetThemeColor(FromHex("#49ff4a")); - public static ImColor Blue => GetThemeColor(FromHex("#49a3ff")); - public static ImColor Yellow => GetThemeColor(FromHex("#ecff49")); - public static ImColor Cyan => GetThemeColor(FromHex("#49feff")); - public static ImColor Magenta => GetThemeColor(FromHex("#ff49fe")); - public static ImColor Orange => GetThemeColor(FromHex("#ffa549")); - public static ImColor Pink => GetThemeColor(FromHex("#ff49a3")); - public static ImColor Lime => GetThemeColor(FromHex("#a3ff49")); - public static ImColor Purple => GetThemeColor(FromHex("#c949ff")); -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - - /// - /// Neutral colors for backgrounds, borders, and subtle elements. - /// These try to find close colors in the current theme while preserving the intended lightness. - /// - public static class Neutral - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor White => GetThemeColor(FromHex("#ffffff")); - public static ImColor Black => GetThemeColor(FromHex("#000000")); - public static ImColor Gray => GetThemeColor(FromHex("#808080")); - public static ImColor LightGray => GetThemeColor(FromHex("#c0c0c0")); - public static ImColor DarkGray => GetThemeColor(FromHex("#404040")); - public static ImColor Transparent => FromHex("#00000000"); // Always transparent -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - - /// - /// Semantic colors for UI states and meanings. - /// These are mapped directly to their semantic meanings in the current theme. - /// - public static class Semantic - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor Error => GetSemanticColor(SemanticMeaning.Error, Priority.High, Basic.Red); - public static ImColor Warning => GetSemanticColor(SemanticMeaning.Warning, Priority.High, Basic.Orange); - public static ImColor Success => GetSemanticColor(SemanticMeaning.Success, Priority.High, Basic.Green); - public static ImColor Info => GetSemanticColor(SemanticMeaning.Information, Priority.High, Basic.Cyan); - public static ImColor Primary => GetSemanticColor(SemanticMeaning.Primary, Priority.High, Basic.Blue); - public static ImColor Secondary => GetSemanticColor(SemanticMeaning.Alternate, Priority.High, Basic.Purple); -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - - /// - /// Natural and earthy colors. - /// These try to find close colors in the current theme while preserving the intended natural hue. - /// - public static class Natural - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor Brown => GetThemeColor(FromRGB(165, 42, 42)); - public static ImColor Olive => GetThemeColor(FromRGB(128, 128, 0)); - public static ImColor Maroon => GetThemeColor(FromRGB(128, 0, 0)); - public static ImColor Navy => GetThemeColor(FromRGB(0, 0, 128)); - public static ImColor Teal => GetThemeColor(FromRGB(0, 128, 128)); - public static ImColor Indigo => GetThemeColor(FromRGB(75, 0, 130)); -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - - /// - /// Vibrant and colorful shades. - /// These try to find close colors in the current theme while preserving the intended vibrant character. - /// - public static class Vibrant - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor Coral => GetThemeColor(FromRGB(255, 127, 80)); - public static ImColor Salmon => GetThemeColor(FromRGB(250, 128, 114)); - public static ImColor Turquoise => GetThemeColor(FromRGB(64, 224, 208)); - public static ImColor Violet => GetThemeColor(FromRGB(238, 130, 238)); - public static ImColor Gold => GetThemeColor(FromRGB(255, 215, 0)); - public static ImColor Silver => GetThemeColor(FromRGB(192, 192, 192)); -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - - /// - /// Soft, pastel colors for gentle UIs. - /// These try to find close colors in the current theme while preserving the intended pastel softness. - /// - public static class Pastel - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static ImColor Beige => GetThemeColor(FromRGB(245, 245, 220)); - public static ImColor Peach => GetThemeColor(FromRGB(255, 218, 185)); - public static ImColor Mint => GetThemeColor(FromRGB(189, 252, 201)); - public static ImColor Lavender => GetThemeColor(FromRGB(230, 230, 250)); - public static ImColor Khaki => GetThemeColor(FromRGB(240, 230, 140)); - public static ImColor Plum => GetThemeColor(FromRGB(221, 160, 221)); -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } - } -} diff --git a/ImGui.Styler/ColorExtensions.cs b/ImGui.Styler/ColorExtensions.cs deleted file mode 100644 index 35e3009..0000000 --- a/ImGui.Styler/ColorExtensions.cs +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Numerics; -using Hexa.NET.ImGui; - -/// -/// Extension methods for color manipulation and analysis. -/// -public static class ColorExtensions -{ - /// - /// Desaturates the color by a specified amount. - /// - /// The original color. - /// The amount to desaturate (0-1). - /// A new object with the adjusted saturation. - public static ImColor DesaturateBy(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Y = Math.Clamp(hsla.Y - amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Saturates the color by a specified amount. - /// - /// The original color. - /// The amount to saturate (0-1). - /// A new object with the adjusted saturation. - public static ImColor SaturateBy(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Y = Math.Clamp(hsla.Y + amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Sets the saturation of the color to a specified amount. - /// - /// The original color. - /// The new saturation value (0-1). - /// A new object with the adjusted saturation. - public static ImColor WithSaturation(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Y = Math.Clamp(amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Multiplies the saturation of the color by a specified amount. - /// - /// The original color. - /// The factor to multiply the saturation by. - /// A new object with the adjusted saturation. - public static ImColor MultiplySaturation(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Y = Math.Clamp(hsla.Y * amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Offsets the hue of the color by a specified amount. - /// - /// The original color. - /// The amount to offset the hue by (0-1). - /// A new object with the adjusted hue. - public static ImColor OffsetHue(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.X = (1f + (hsla.X + amount)) % 1f; - return Color.FromHSLA(hsla); - } - - /// - /// Lightens the color by a specified amount. - /// - /// The original color. - /// The amount to lighten the color by (0-1). - /// A new object with the adjusted lightness. - public static ImColor LightenBy(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Z = Math.Clamp(hsla.Z + amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Darkens the color by a specified amount. - /// - /// The original color. - /// The amount to darken the color by (0-1). - /// A new object with the adjusted lightness. - public static ImColor DarkenBy(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Z = Math.Clamp(hsla.Z - amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Sets the luminance of the color to a specified amount. - /// - /// The original color. - /// The new luminance value (0-1). - /// A new object with the adjusted luminance. - public static ImColor WithLuminance(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Z = Math.Clamp(amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Multiplies the luminance of the color by a specified amount. - /// - /// The original color. - /// The factor to multiply the luminance by. - /// A new object with the adjusted luminance. - public static ImColor MultiplyLuminance(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.Z = Math.Clamp(hsla.Z * amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Sets the alpha of the color to a specified amount. - /// - /// The original color. - /// The new alpha value (0-1). - /// A new object with the adjusted alpha. - public static ImColor WithAlpha(this ImColor color, float amount) - { - Vector4 hsla = color.ToHSLA(); - hsla.W = Math.Clamp(amount, 0, 1); - return Color.FromHSLA(hsla); - } - - /// - /// Converts the color to grayscale. - /// - /// The original color. - /// A new object in grayscale. - public static ImColor ToGrayscale(this ImColor color) => color.WithSaturation(0); - - /// - /// Converts the color to HSLA (Hue, Saturation, Lightness, Alpha) format. - /// - /// The original color. - /// A representing the color in HSLA format. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Clarity over brevity")] - public static Vector4 ToHSLA(this ImColor color) - { - float r = color.Value.X; - float g = color.Value.Y; - float b = color.Value.Z; - float a = color.Value.W; - - float max = Math.Max(r, Math.Max(g, b)); - float min = Math.Min(r, Math.Min(g, b)); - float h, s, l = (max + min) / 2f; - - if (max == min) - { - h = s = 0; - } - else - { - float d = max - min; - s = l > 0.5f ? d / (2f - max - min) : d / (max + min); - if (max == r) - { - h = (g - b) / d; - } - else if (max == g) - { - h = ((b - r) / d) + 2; - } - else - { - h = ((r - g) / d) + 4; - } - - h /= 6; - if (h < 0) - { - h += 1; - } - } - - return new Vector4(h, s, l, a); - } - - /// - /// Gets the relative luminance of the color. - /// - /// The original color. - /// The relative luminance of the color. - public static float GetRelativeLuminance(this ImColor color) => - (color.Value.X * 0.2126f) + (color.Value.Y * 0.7152f) + (color.Value.Z * 0.0722f); - - /// - /// Calculates the contrast ratio of the color over a background color. - /// - /// The original color. - /// The background color. - /// The contrast ratio of the color over the background color. - public static float GetContrastRatioOver(this ImColor color, ImColor background) - { - float relativeLuminance = color.GetRelativeLuminance(); - float backgroundRelativeLuminance = background.GetRelativeLuminance(); - - // Ensure lighter color is in numerator for proper contrast ratio calculation - float lighter = Math.Max(relativeLuminance, backgroundRelativeLuminance); - float darker = Math.Min(relativeLuminance, backgroundRelativeLuminance); - - return (lighter + 0.05f) / (darker + 0.05f); - } - - /// - /// Calculates the optimal contrasting color for the given color. - /// - /// The original color. - /// A new object representing the optimal contrasting color. - public static ImColor CalculateOptimalContrastingColor(this ImColor color) - { - // Try pure white and pure black first as they often provide the best contrast - ImColor white = Color.FromRGBA(1f, 1f, 1f, 1f); - ImColor black = Color.FromRGBA(0f, 0f, 0f, 1f); - - float whiteContrast = white.GetContrastRatioOver(color); - float blackContrast = black.GetContrastRatioOver(color); - - // If either pure white or black provides sufficient contrast, use the better one - if (whiteContrast >= Color.OptimalTextContrastRatio || blackContrast >= Color.OptimalTextContrastRatio) - { - return whiteContrast > blackContrast ? white : black; - } - - // Otherwise, search for optimal luminance - float bestLuminance = 0; - float bestContrast = 0; - int steps = 256; - - for (int i = 0; i < steps; i++) - { - float l = i / (steps - 1f); - ImColor candidateColor = color.WithLuminance(l); - float contrast = candidateColor.GetContrastRatioOver(color); - - if (contrast > bestContrast) - { - bestContrast = contrast; - bestLuminance = l; - } - } - - return color.WithLuminance(bestLuminance); - } - - /// - /// Adjusts the background color to provide sufficient contrast for the given text color. - /// - /// The background color to adjust. - /// The text color that needs to be readable. - /// The target contrast ratio. If not specified, uses the optimal text contrast ratio. - /// A new object representing the adjusted background color. - public static ImColor AdjustForSufficientContrast(this ImColor backgroundColor, ImColor textColor, float? targetContrastRatio = null) - { - float targetRatio = targetContrastRatio ?? Color.OptimalTextContrastRatio; - float currentContrast = textColor.GetContrastRatioOver(backgroundColor); - - // If contrast is already sufficient, return the original color - if (currentContrast >= targetRatio) - { - return backgroundColor; - } - - // Search for optimal luminance that provides the target contrast - float bestLuminance = backgroundColor.ToHSLA().Z; - float bestContrast = currentContrast; - int steps = 256; - - // Try different luminance values to find one that provides sufficient contrast - for (int i = 0; i < steps; i++) - { - float l = i / (steps - 1f); - ImColor candidateBackground = backgroundColor.WithLuminance(l); - float contrast = textColor.GetContrastRatioOver(candidateBackground); - - // If we found sufficient contrast, prefer the luminance closest to original - if (contrast >= targetRatio) - { - float luminanceDifference = Math.Abs(l - backgroundColor.ToHSLA().Z); - float currentBestDifference = Math.Abs(bestLuminance - backgroundColor.ToHSLA().Z); - - if (contrast > bestContrast || - (contrast >= targetRatio && luminanceDifference < currentBestDifference)) - { - bestContrast = contrast; - bestLuminance = l; - } - } - } - - return backgroundColor.WithLuminance(bestLuminance); - } - - /// - /// Calculates the color distance between two colors using Euclidean distance in RGB space. - /// - /// The first color. - /// The second color. - /// The color distance between the two colors (0.0 to ~1.73). - public static float GetColorDistance(this ImColor color1, ImColor color2) - { - float deltaR = color1.Value.X - color2.Value.X; - float deltaG = color1.Value.Y - color2.Value.Y; - float deltaB = color1.Value.Z - color2.Value.Z; - - return (float)Math.Sqrt((deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB)); - } -} diff --git a/ImGui.Styler/CompatibilitySuppressions.xml b/ImGui.Styler/CompatibilitySuppressions.xml deleted file mode 100644 index c54eb32..0000000 --- a/ImGui.Styler/CompatibilitySuppressions.xml +++ /dev/null @@ -1,52 +0,0 @@ -īģŋ - - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureGuardAttribute - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0001 - T:System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0001 - T:System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0001 - T:System.Runtime.CompilerServices.ParamCollectionAttribute - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0001 - T:System.Threading.Lock - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_UrlFormat:[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_UrlFormat(System.String):[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Styler.dll - lib/net9.0/ktsu.ImGui.Styler.dll - - \ No newline at end of file diff --git a/ImGui.Styler/DESCRIPTION.md b/ImGui.Styler/DESCRIPTION.md deleted file mode 100644 index fe9a195..0000000 --- a/ImGui.Styler/DESCRIPTION.md +++ /dev/null @@ -1 +0,0 @@ -A library for expressively styling ImGui.NET interfaces. diff --git a/ImGui.Styler/ImGui.Styler.csproj b/ImGui.Styler/ImGui.Styler.csproj deleted file mode 100644 index b0b090e..0000000 --- a/ImGui.Styler/ImGui.Styler.csproj +++ /dev/null @@ -1,17 +0,0 @@ -īģŋ - - - - - true - net8.0;net9.0; - - - - - - - - - - diff --git a/ImGui.Styler/Indent.cs b/ImGui.Styler/Indent.cs deleted file mode 100644 index 0909b39..0000000 --- a/ImGui.Styler/Indent.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using Hexa.NET.ImGui; - -using ktsu.ScopedAction; - -/// -/// Provides methods to create scoped indents for ImGui. -/// -public static class Indent -{ - /// - /// Creates a scoped indent with the default width. - /// - /// A new instance of . - public static ScopedIndent ByDefault() => new(); - - /// - /// Creates a scoped indent with a specified width. - /// - /// The width of the indent. - /// A new instance of . - public static ScopedIndentBy By(float width) => new(width); - - /// - /// Represents a scoped indent action. - /// - public class ScopedIndent : ScopedAction - { - /// - /// Initializes a new instance of the class. - /// - public ScopedIndent() : base(ImGui.Indent, ImGui.Unindent) { } - } - - /// - /// Represents a scoped indent action with a specified width. - /// - public class ScopedIndentBy : ScopedStyleVar - { - /// - /// Initializes a new instance of the class with a specified width. - /// - /// The width of the indent. - public ScopedIndentBy(float width) : base(ImGuiStyleVar.IndentSpacing, width) - { - ImGui.Indent(width); - Action? onClose = OnClose; - OnClose = () => - { - ImGui.Unindent(width); - onClose?.Invoke(); - }; - } - } -} diff --git a/ImGui.Styler/README.md b/ImGui.Styler/README.md deleted file mode 100644 index 1e729a9..0000000 --- a/ImGui.Styler/README.md +++ /dev/null @@ -1,350 +0,0 @@ -# ImGuiStyler 🎨 - -[![Version](https://img.shields.io/badge/version-1.3.10-blue.svg)](VERSION.md) -[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md) - -**A powerful, expressive styling library for ImGui.NET interfaces** that simplifies theme management, provides scoped styling utilities, and offers advanced color manipulation with accessibility features. - -## ✨ Features - -### 🎨 **Advanced Theme System** -- **50+ Built-in Themes**: Comprehensive collection including Catppuccin, Dracula, Gruvbox, Tokyo Night, Nord, and many more -- **Interactive Theme Browser**: Visual theme selection with live preview and categorization -- **Semantic Theme Support**: Leverages `ktsu.ThemeProvider` for consistent, semantic color theming -- **Scoped Theme Application**: Apply themes to specific UI sections without affecting the global style - -### đŸŽ¯ **Precise Alignment Tools** -- **Automatic Content Centering**: Center any content within containers or available regions -- **Flexible Container Alignment**: Align content within custom-sized containers -- **Layout Integration**: Seamlessly works with ImGui's existing layout system - -### 🌈 **Advanced Color Management** -- **Hex Color Support**: Direct conversion from hex strings to ImGui colors -- **Accessibility-First**: Automatic contrast calculation and optimal text color selection -- **Color Manipulation**: Lighten, darken, and adjust colors programmatically -- **Scoped Color Application**: Apply colors to specific UI elements without side effects - -### 🔧 **Scoped Styling System** -- **Style Variables**: Apply temporary style modifications with automatic cleanup -- **Text Colors**: Scoped text color changes with proper restoration -- **Theme Colors**: Apply theme-based colors to specific UI sections -- **Memory Safe**: Automatic resource management and style restoration - -## đŸ“Ļ Installation - -Add ImGuiStyler to your project via NuGet: - -```xml - -``` - -Or via Package Manager Console: -```powershell -Install-Package ktsu.ImGuiStyler -``` - -## 🚀 Quick Start - -```csharp -using ktsu.ImGuiStyler; -using Hexa.NET.ImGui; - -// Apply a global theme -Theme.Apply("TokyoNight"); - -// Use scoped styling for specific elements -using (new ScopedColor(ImGuiCol.Text, Color.FromHex("#ff6b6b"))) -{ - ImGui.Text("This text is red!"); -} - -// Center content automatically -using (new Alignment.Center(ImGui.CalcTextSize("Centered!"))) -{ - ImGui.Text("Centered!"); -} -``` - -## 📚 Comprehensive Usage Guide - -### 🎨 Theme Management - -#### Applying Global Themes -```csharp -// Apply any of the 50+ built-in themes -Theme.Apply("Catppuccin.Mocha"); -Theme.Apply("Gruvbox.Dark"); -Theme.Apply("Tokyo Night"); - -// Get current theme information -string? currentTheme = Theme.CurrentThemeName; -bool isCurrentThemeDark = Theme.IsCurrentThemeDark; - -// Reset to default ImGui theme -Theme.Reset(); -``` - -#### Interactive Theme Browser -```csharp -// Show the theme browser modal -if (ImGui.Button("Choose Theme")) -{ - Theme.ShowThemeSelector("Select a Theme"); -} - -// Render the theme selector (call this in your main render loop) -if (Theme.RenderThemeSelector()) -{ - Console.WriteLine($"Theme changed to: {Theme.CurrentThemeName}"); -} -``` - -#### Scoped Theme Application -```csharp -using (new ScopedTheme("Dracula")) -{ - ImGui.Text("This text uses Dracula theme"); - ImGui.Button("Themed button"); - - using (new ScopedTheme("Nord")) - { - ImGui.Text("Nested Nord theme"); - } - // Automatically reverts to Dracula -} -// Automatically reverts to previous theme -``` - -### 🌈 Color Management - -#### Creating Colors -```csharp -// From hex strings -ImColor red = Color.FromHex("#ff0000"); -ImColor blueWithAlpha = Color.FromHex("#0066ffcc"); - -// From RGB values -ImColor green = Color.FromRGB(0, 255, 0); -ImColor customColor = Color.FromRGBA(255, 128, 64, 200); - -// From HSV -ImColor rainbow = Color.FromHSV(0.83f, 1.0f, 1.0f); // Purple -``` - -#### Color Manipulation -```csharp -ImColor baseColor = Color.FromHex("#3498db"); - -// Adjust brightness -ImColor lighter = Color.Lighten(baseColor, 0.3f); -ImColor darker = Color.Darken(baseColor, 0.2f); - -// Accessibility-focused text colors -ImColor optimalText = Color.GetOptimalTextColor(baseColor); -ImColor contrastText = Color.GetContrastingTextColor(baseColor); -``` - -#### Scoped Color Application -```csharp -// Scoped text color -using (new ScopedTextColor(Color.FromHex("#e74c3c"))) -{ - ImGui.Text("Red text"); -} - -// Scoped UI element color -using (new ScopedColor(ImGuiCol.Button, Color.FromHex("#2ecc71"))) -{ - ImGui.Button("Green button"); -} - -// Multiple scoped colors -using (new ScopedColor(ImGuiCol.Button, Color.FromHex("#9b59b6"))) -using (new ScopedColor(ImGuiCol.ButtonHovered, Color.FromHex("#8e44ad"))) -using (new ScopedColor(ImGuiCol.ButtonActive, Color.FromHex("#71368a"))) -{ - ImGui.Button("Fully styled button"); -} -``` - -### đŸŽ¯ Alignment and Layout - -#### Content Centering -```csharp -// Center text -string text = "Perfectly centered!"; -using (new Alignment.Center(ImGui.CalcTextSize(text))) -{ - ImGui.Text(text); -} - -// Center buttons -using (new Alignment.Center(new Vector2(120, 30))) -{ - ImGui.Button("Centered Button", new Vector2(120, 30)); -} -``` - -#### Custom Container Alignment -```csharp -Vector2 containerSize = new(400, 200); -Vector2 contentSize = new(100, 50); - -// Center content within a specific container -using (new Alignment.CenterWithin(contentSize, containerSize)) -{ - ImGui.Button("Centered in Container", contentSize); -} -``` - -### 🔧 Advanced Styling - -#### Scoped Style Variables -```csharp -// Rounded buttons -using (new ScopedStyleVar(ImGuiStyleVar.FrameRounding, 8.0f)) -{ - ImGui.Button("Rounded Button"); -} - -// Multiple style modifications -using (new ScopedStyleVar(ImGuiStyleVar.FrameRounding, 12.0f)) -using (new ScopedStyleVar(ImGuiStyleVar.FramePadding, new Vector2(20, 10))) -using (new ScopedStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10, 8))) -{ - ImGui.Button("Highly Styled Button"); - ImGui.Button("Another Styled Button"); -} -``` - -#### Theme-Based Styling -```csharp -// Use semantic colors from current theme -using (new ScopedThemeColor(Color.Primary)) -{ - ImGui.Text("Primary theme color"); -} - -using (new ScopedThemeColor(Color.Secondary)) -{ - ImGui.Button("Secondary theme button"); -} -``` - -## 🎨 Available Themes - -ImGuiStyler includes **50+ carefully crafted themes** across multiple families: - -### 🌙 Dark Themes -- **Catppuccin**: Mocha, Macchiato, Frappe -- **Tokyo Night**: Classic, Storm -- **Gruvbox**: Dark, Dark Hard, Dark Soft -- **Dracula**: Classic vampire theme -- **Nord**: Arctic, frost-inspired theme -- **Nightfox**: Carbonfox, Nightfox, Terafox -- **OneDark**: Popular dark theme -- **Kanagawa**: Wave, Dragon variants -- **Everforest**: Dark, Dark Hard, Dark Soft - -### â˜€ī¸ Light Themes -- **Catppuccin**: Latte -- **Tokyo Night**: Day -- **Gruvbox**: Light, Light Hard, Light Soft -- **Nord**: Light variant -- **Nightfox**: Dawnfox, Dayfox -- **PaperColor**: Light -- **Everforest**: Light, Light Hard, Light Soft -- **VSCode**: Light theme - -### 🎨 Specialty Themes -- **Monokai**: Classic editor theme -- **Nightfly**: Smooth dark theme -- **VSCode**: Dark theme recreation - -## đŸ› ī¸ API Reference - -### Theme Class -- `Theme.Apply(string themeName)` - Apply a global theme -- `Theme.Apply(ISemanticTheme theme)` - Apply a semantic theme -- `Theme.Reset()` - Reset to default ImGui theme -- `Theme.ShowThemeSelector(string title)` - Show theme browser modal -- `Theme.RenderThemeSelector()` - Render theme browser (returns true if theme changed) -- `Theme.AllThemes` - Get all available themes -- `Theme.Families` - Get all theme families -- `Theme.CurrentThemeName` - Get current theme name -- `Theme.IsCurrentThemeDark` - Check if current theme is dark - -### Color Class -- `Color.FromHex(string hex)` - Create color from hex string -- `Color.FromRGB(int r, int g, int b)` - Create color from RGB -- `Color.FromRGBA(int r, int g, int b, int a)` - Create color from RGBA -- `Color.GetOptimalTextColor(ImColor background)` - Get accessible text color -- `Color.Lighten(ImColor color, float amount)` - Lighten color -- `Color.Darken(ImColor color, float amount)` - Darken color - -### Alignment Classes -- `new Alignment.Center(Vector2 contentSize)` - Center in available region -- `new Alignment.CenterWithin(Vector2 contentSize, Vector2 containerSize)` - Center in container - -### Scoped Classes -- `new ScopedColor(ImGuiCol col, ImColor color)` - Scoped color application -- `new ScopedTextColor(ImColor color)` - Scoped text color -- `new ScopedStyleVar(ImGuiStyleVar var, float value)` - Scoped style variable -- `new ScopedTheme(string themeName)` - Scoped theme application -- `new ScopedThemeColor(Color semanticColor)` - Scoped semantic color - -## đŸŽ¯ Demo Application - -The included demo application showcases all features: - -```bash -cd ImGuiStylerDemo -dotnet run -``` - -Features demonstrated: -- Interactive theme browser with live preview -- All 50+ themes with family categorization -- Scoped styling examples -- Color manipulation demos -- Alignment showcases -- Accessibility features - -## 🤝 Contributing - -We welcome contributions! Please see our contributing guidelines: - -1. **Fork** the repository -2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) -3. **Commit** your changes (`git commit -m 'Add amazing feature'`) -4. **Push** to the branch (`git push origin feature/amazing-feature`) -5. **Open** a Pull Request - -### Development Setup -```bash -git clone https://github.com/ktsu-dev/ImGuiStyler.git -cd ImGuiStyler -dotnet restore -dotnet build -``` - -## 📄 License - -This project is licensed under the **MIT License** - see the [LICENSE.md](LICENSE.md) file for details. - -## 🙏 Acknowledgments - -- **[ImGui.NET](https://github.com/mellinoe/ImGui.NET)** - .NET bindings for Dear ImGui -- **[Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui)** - Modern ImGui bindings -- **Theme Inspirations**: Catppuccin, Tokyo Night, Gruvbox, and other amazing color schemes -- **Community Contributors** - Thank you for your themes, bug reports, and improvements! - -## 🔗 Related Projects - -- **[ktsu.ThemeProvider](https://github.com/ktsu-dev/ThemeProvider)** - Semantic theming foundation -- **[ktsu.ImGuiPopups](https://github.com/ktsu-dev/ImGuiPopups)** - Modal and popup utilities -- **[ktsu.Extensions](https://github.com/ktsu-dev/Extensions)** - Utility extensions - ---- - -**Made with â¤ī¸ by the ktsu.dev team** diff --git a/ImGui.Styler/ScopedColor.cs b/ImGui.Styler/ScopedColor.cs deleted file mode 100644 index f9d452c..0000000 --- a/ImGui.Styler/ScopedColor.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using Hexa.NET.ImGui; - -using ktsu.ScopedAction; - -/// -/// Represents a scoped color change in ImGui. -/// -/// -/// This class ensures that the color change is reverted when the scope ends. -/// -public class ScopedColor : ScopedAction -{ - /// - /// Initializes a new instance of the class with a specified target and color. - /// - /// The ImGui color target to change. - /// The color to apply to the target. - public ScopedColor(ImGuiCol target, ImColor color) : base( - onOpen: () => ImGui.PushStyleColor(target, color.Value), - onClose: ImGui.PopStyleColor) - { - } - - /// - /// Initializes a new instance of the class with a specified color for the button. - /// - /// The color to apply to the button. - public ScopedColor(ImColor color) - { - ImGui.PushStyleColor(ImGuiCol.Button, color.Value); - OnClose = ImGui.PopStyleColor; - } -} diff --git a/ImGui.Styler/ScopedStyleVar.cs b/ImGui.Styler/ScopedStyleVar.cs deleted file mode 100644 index 0f63a93..0000000 --- a/ImGui.Styler/ScopedStyleVar.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ScopedAction; - -/// -/// Represents a scoped style variable for ImGui. This class ensures that the style variable is popped when disposed. -/// -public class ScopedStyleVar : ScopedAction -{ - /// - /// Initializes a new instance of the class with a vector value. - /// - /// The style variable to be pushed. - /// The vector value to be applied to the style variable. - public ScopedStyleVar(ImGuiStyleVar target, Vector2 vector) - { - ImGui.PushStyleVar(target, vector); - OnClose = ImGui.PopStyleVar; - } - - /// - /// Initializes a new instance of the class with a scalar value. - /// - /// The style variable to be pushed. - /// The scalar value to be applied to the style variable. - public ScopedStyleVar(ImGuiStyleVar target, float scalar) - { - ImGui.PushStyleVar(target, scalar); - OnClose = ImGui.PopStyleVar; - } -} diff --git a/ImGui.Styler/ScopedTextColor.cs b/ImGui.Styler/ScopedTextColor.cs deleted file mode 100644 index 9682faf..0000000 --- a/ImGui.Styler/ScopedTextColor.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; -using Hexa.NET.ImGui; - -/// -/// Represents a scoped text color change in ImGui. -/// -/// The color to apply to the text. -public class ScopedTextColor(ImColor color) : ScopedColor(ImGuiCol.Text, color) -{ -} diff --git a/ImGui.Styler/ScopedTheme.cs b/ImGui.Styler/ScopedTheme.cs deleted file mode 100644 index 9c3f6a5..0000000 --- a/ImGui.Styler/ScopedTheme.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Collections.Concurrent; -using System.Numerics; -using Hexa.NET.ImGui; -using ktsu.ScopedAction; -using ktsu.ThemeProvider; - -/// -/// Represents a scoped action that applies a complete semantic theme to ImGui elements. -/// This provides semantic theme-based styling that automatically reverts when disposed. -/// -public class ScopedTheme : ScopedAction -{ - /// - /// Cache for storing computed color mappings per theme to avoid repeated computation. - /// - private static readonly ConcurrentDictionary> colorMappingCache = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The semantic theme to apply. - public ScopedTheme(ISemanticTheme theme) - { - ArgumentNullException.ThrowIfNull(theme); - - // Create a cache key based on the theme type name (assumes one theme per type) - string cacheKey = theme.GetType().FullName ?? theme.GetType().Name; - - // Get the color mapping from cache or compute it - IReadOnlyDictionary colorMapping = colorMappingCache.GetOrAdd(cacheKey, _ => Theme.GetColorMapping(theme)); - - int numStyles = 0; - - // Apply all mapped colors using PushStyleAndCount - foreach ((ImGuiCol imguiCol, Vector4 color) in colorMapping) - { - PushStyleAndCount(imguiCol, color, ref numStyles); - } - - OnClose = () => ImGui.PopStyleColor(numStyles); - } - - /// - /// Clears the color mapping cache. This can be useful if themes have been modified - /// or to free memory if many different themes have been used. - /// - public static void ClearCache() => colorMappingCache.Clear(); - - private static void PushStyleAndCount(ImGuiCol style, Vector4 color, ref int numStyles) - { - ImGui.PushStyleColor(style, color); - ++numStyles; - } -} diff --git a/ImGui.Styler/ScopedThemeColor.cs b/ImGui.Styler/ScopedThemeColor.cs deleted file mode 100644 index c93ff60..0000000 --- a/ImGui.Styler/ScopedThemeColor.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using Hexa.NET.ImGui; -using ktsu.ScopedAction; - -/// -/// Represents a scoped action that applies a theme color to ImGui elements. -/// This provides a simple color-based theming that automatically reverts when disposed. -/// -public class ScopedThemeColor : ScopedAction -{ - /// - /// Initializes a new instance of the class. - /// - /// The base color to apply to the theme. - /// Whether the theme should be enabled or disabled. - public ScopedThemeColor(ImColor baseColor, bool enabled) - { - // Simple color adjustments for basic theming - ImColor primaryColor = enabled ? baseColor : baseColor.MultiplySaturation(0.3f); - ImColor hoveredColor = primaryColor.MultiplyLuminance(1.2f); - ImColor activeColor = primaryColor.MultiplyLuminance(0.8f); - ImColor textColor = primaryColor.CalculateOptimalContrastingColor(); - ImColor backgroundColor = primaryColor.MultiplyLuminance(0.1f).MultiplySaturation(0.1f); - - int numStyles = 0; - PushStyleAndCount(ImGuiCol.Text, textColor, ref numStyles); - PushStyleAndCount(ImGuiCol.TextSelectedBg, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.TextDisabled, textColor.MultiplySaturation(0.5f), ref numStyles); - PushStyleAndCount(ImGuiCol.Button, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.ButtonActive, activeColor, ref numStyles); - PushStyleAndCount(ImGuiCol.ButtonHovered, hoveredColor, ref numStyles); - PushStyleAndCount(ImGuiCol.CheckMark, textColor, ref numStyles); - PushStyleAndCount(ImGuiCol.Header, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.HeaderActive, activeColor, ref numStyles); - PushStyleAndCount(ImGuiCol.HeaderHovered, hoveredColor, ref numStyles); - PushStyleAndCount(ImGuiCol.SliderGrab, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.SliderGrabActive, activeColor, ref numStyles); - PushStyleAndCount(ImGuiCol.Tab, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.TabSelected, activeColor, ref numStyles); - PushStyleAndCount(ImGuiCol.TabHovered, hoveredColor, ref numStyles); - PushStyleAndCount(ImGuiCol.FrameBg, primaryColor, ref numStyles); - PushStyleAndCount(ImGuiCol.FrameBgActive, activeColor, ref numStyles); - PushStyleAndCount(ImGuiCol.FrameBgHovered, hoveredColor, ref numStyles); - PushStyleAndCount(ImGuiCol.WindowBg, backgroundColor, ref numStyles); - PushStyleAndCount(ImGuiCol.ChildBg, backgroundColor, ref numStyles); - PushStyleAndCount(ImGuiCol.PopupBg, backgroundColor, ref numStyles); - - OnClose = () => ImGui.PopStyleColor(numStyles); - } - - private static void PushStyleAndCount(ImGuiCol style, ImColor color, ref int numStyles) - { - ImGui.PushStyleColor(style, color.Value); - ++numStyles; - } -} diff --git a/ImGui.Styler/Text.cs b/ImGui.Styler/Text.cs deleted file mode 100644 index 7545696..0000000 --- a/ImGui.Styler/Text.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; -using Hexa.NET.ImGui; - -/// -/// Provides functionality for managing text in ImGui. -/// -public static partial class Text -{ - /// - /// Provides functionality for managing text colors in ImGui. - /// - public static partial class Color - { - /// - /// Contains predefined color definitions for text. - /// - public static class Definitions - { - /// - /// Gets or sets the normal text color. - /// - public static ImColor Normal { get; set; } = Styler.Color.Palette.Neutral.White; - - /// - /// Gets or sets the error text color. - /// - public static ImColor Error { get; set; } = Styler.Color.Palette.Basic.Red; - - /// - /// Gets or sets the warning text color. - /// - public static ImColor Warning { get; set; } = Styler.Color.Palette.Basic.Yellow; - - /// - /// Gets or sets the info text color. - /// - public static ImColor Info { get; set; } = Styler.Color.Palette.Basic.Cyan; - - /// - /// Gets or sets the success text color. - /// - public static ImColor Success { get; set; } = Styler.Color.Palette.Basic.Green; - } - - /// - /// Applies the normal text color within a scoped block. - /// - /// A instance that reverts the color when disposed. - public static ScopedTextColor Normal() => new(Definitions.Normal); - - /// - /// Applies the error text color within a scoped block. - /// - /// A instance that reverts the color when disposed. - public static ScopedTextColor Error() => new(Definitions.Error); - - /// - /// Applies the warning text color within a scoped block. - /// - /// A instance that reverts the color when disposed. - public static ScopedTextColor Warning() => new(Definitions.Warning); - - /// - /// Applies the info text color within a scoped block. - /// - /// A instance that reverts the color when disposed. - public static ScopedTextColor Info() => new(Definitions.Info); - - /// - /// Applies the success text color within a scoped block. - /// - /// A instance that reverts the color when disposed. - public static ScopedTextColor Success() => new(Definitions.Success); - } -} diff --git a/ImGui.Styler/Theme.cs b/ImGui.Styler/Theme.cs deleted file mode 100644 index c63ff93..0000000 --- a/ImGui.Styler/Theme.cs +++ /dev/null @@ -1,1304 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.Numerics; -using Hexa.NET.ImGui; -using ktsu.ThemeProvider; -using ktsu.ThemeProvider.ImGui; - -/// -/// Provides methods and properties to manage and apply themes for ImGui elements using ThemeProvider. -/// -public static class Theme -{ - private static readonly ImGuiPaletteMapper paletteMapper = new(); - private static string? currentThemeName; - - // Cache for complete palettes to avoid recalculating them every frame - private static readonly Dictionary> paletteCache = []; - private static readonly Lock paletteCacheLock = new(); - - // ThemeBrowser modal instance - private static readonly ThemeBrowser themeBrowser = new(); - - #region Theme Application - - /// - /// Applies a semantic theme to ImGui using ThemeProvider's color mapping system. - /// - /// The semantic theme to apply. - public static void Apply(ISemanticTheme theme) - { - ArgumentNullException.ThrowIfNull(theme); - - // Map the theme to ImGui colors - IReadOnlyDictionary colorMapping = paletteMapper.MapTheme(theme); - - // Apply all colors to ImGui - unsafe - { - ImGuiStylePtr style = ImGui.GetStyle(); - foreach ((ImGuiCol imguiCol, Vector4 color) in colorMapping) - { - style.Colors[(int)imguiCol] = color; - } - } - } - - /// - /// Gets the color mapping for a semantic theme without applying it. - /// This is useful for temporary theme application via scoped actions. - /// - /// The semantic theme to get the color mapping for. - /// A dictionary mapping ImGui colors to their theme-based values. - public static IReadOnlyDictionary GetColorMapping(ISemanticTheme theme) - { - ArgumentNullException.ThrowIfNull(theme); - return paletteMapper.MapTheme(theme); - } - - /// - /// Applies a theme by name using ThemeRegistry. - /// - /// The name of the theme to apply. - /// True if the theme was found and applied, false otherwise. - public static bool Apply(string themeName) - { - ThemeRegistry.ThemeInfo? themeInfo = ThemeRegistry.FindTheme(themeName); - if (themeInfo is null) - { - return false; - } - - Apply(themeInfo.CreateInstance()); - currentThemeName = themeName; - - // Clear palette cache when theme changes - ClearPaletteCache(); - return true; - } - - /// - /// Resets ImGui to default styling with no theme applied. - /// - public static void ResetToDefault() - { - // Use ImGui's built-in classic color scheme to restore proper defaults - ImGui.StyleColorsDark(); - currentThemeName = null; - - // Clear palette cache when resetting theme - ClearPaletteCache(); - } - - #endregion - - #region Current Theme Tracking - - /// - /// Gets or sets the name of the currently selected theme. - /// Setting this will apply the theme if it exists, or reset to default if set to null. - /// - public static string? CurrentThemeName - { - get => currentThemeName; - set - { - if (value is null) - { - ResetToDefault(); - } - else if (Apply(value)) - { - currentThemeName = value; - } - } - } - - /// - /// Gets the ThemeInfo for the currently selected theme, if any. - /// - public static ThemeRegistry.ThemeInfo? CurrentTheme => - currentThemeName is not null ? FindTheme(currentThemeName) : null; - - #endregion - - #region Theme Menu Rendering - - /// - /// Renders a theme selection submenu for use in an application's main menu bar. - /// - /// The label for the theme submenu (default: "Theme") - /// True if a theme was selected and changed, false otherwise. - public static bool RenderMenu(string menuLabel = "Theme") - { - bool themeChanged = false; - - if (ImGui.BeginMenu(menuLabel)) - { - // Reset option at the top - bool isReset = currentThemeName is null; - if (ImGui.MenuItem("Reset to Default", "", isReset)) - { - if (!isReset) - { - ResetToDefault(); - themeChanged = true; - } - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Reset to default ImGui styling with no theme applied"); - } - - ImGui.Separator(); - - // Group themes by family for better organization - IOrderedEnumerable>> themesByFamily = ThemesByFamily.OrderBy(kvp => kvp.Key); - - foreach ((string family, IReadOnlyList themes) in themesByFamily) - { - if (themes.Count == 1) - { - // Single theme - render directly with color swatches - ThemeRegistry.ThemeInfo theme = themes[0]; - bool isSelected = currentThemeName == theme.Name; - - if (RenderThemeMenuItemWithDialogSwatches(theme, theme.Name, isSelected)) - { - if (!isSelected && Apply(theme.Name)) - { - themeChanged = true; - } - } - } - else - { - themeChanged |= RenderFamilySubmenu(family, themes); - } - } - - ImGui.EndMenu(); - } - - return themeChanged; - } - - /// - /// Renders a submenu for a theme family. - /// - /// The theme family name. - /// The themes in the family. - /// True if a theme was selected and changed, false otherwise. - private static bool RenderFamilySubmenu(string family, IReadOnlyList themes) - { - bool themeChanged = false; - - // Use dialog window style for the family header using colors from the first theme - bool anyFamilyThemeSelected = themes.Any(t => t.Name == currentThemeName); - - if (RenderFamilyMenuHeader(family, themes.Count > 0 ? themes[0] : null, anyFamilyThemeSelected)) - { - try - { - // Group by dark/light for families with many variants - if (themes.Count > 4) - { - ThemeRegistry.ThemeInfo[] darkThemes = [.. themes.Where(t => t.IsDark)]; - ThemeRegistry.ThemeInfo[] lightThemes = [.. themes.Where(t => !t.IsDark)]; - - themeChanged |= RenderThemeGroup("Dark", darkThemes); - - if (darkThemes.Length > 0 && lightThemes.Length > 0) - { - ImGui.Separator(); - } - - themeChanged |= RenderThemeGroup("Light", lightThemes); - } - else - { - // Few themes - render directly with color swatches - foreach (ThemeRegistry.ThemeInfo theme in themes) - { - bool isSelected = currentThemeName == theme.Name; - string displayName = theme.Variant ?? theme.Name; - - if (RenderThemeMenuItemWithDialogSwatches(theme, displayName, isSelected)) - { - if (!isSelected && Apply(theme.Name)) - { - themeChanged = true; - } - } - } - } - - ImGui.EndMenu(); - } - finally - { - // Pop the ID that was pushed in RenderFamilyMenuHeader when menu was opened - ImGui.PopID(); - } - } - - return themeChanged; - } - - /// - /// Renders a group of themes (e.g., "Dark", "Light"). - /// - /// The group name. - /// The themes to render. - /// True if a theme was selected and changed, false otherwise. - private static bool RenderThemeGroup(string groupName, ThemeRegistry.ThemeInfo[] themes) - { - bool themeChanged = false; - - if (themes.Length > 0) - { - if (!string.IsNullOrEmpty(groupName)) - { - // Use dialog window style for the group header using colors from the first theme - bool anyGroupThemeSelected = themes.Any(t => t.Name == currentThemeName); - RenderGroupHeader(groupName, themes[0], anyGroupThemeSelected); - ImGui.Separator(); - } - - foreach (ThemeRegistry.ThemeInfo theme in themes) - { - bool isSelected = currentThemeName == theme.Name; - string displayName = theme.Variant ?? theme.Name; - - if (RenderThemeMenuItemWithDialogSwatches(theme, displayName, isSelected)) - { - if (!isSelected && Apply(theme.Name)) - { - themeChanged = true; - } - } - } - } - - return themeChanged; - } - - /// - /// Renders a simple theme selection menu without family grouping. - /// - /// The label for the theme submenu (default: "Theme") - /// True if a theme was selected and changed, false otherwise. - public static bool RenderSimpleMenu(string menuLabel = "Theme") - { - bool themeChanged = false; - - if (ImGui.BeginMenu(menuLabel)) - { - // Reset option at the top - bool isReset = currentThemeName is null; - if (ImGui.MenuItem("Reset to Default", "", isReset)) - { - if (!isReset) - { - ResetToDefault(); - themeChanged = true; - } - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Reset to default ImGui styling with no theme applied"); - } - - ImGui.Separator(); - - // Group by dark/light - ThemeRegistry.ThemeInfo[] darkThemes = [.. DarkThemes.OrderBy(t => t.Name)]; - ThemeRegistry.ThemeInfo[] lightThemes = [.. LightThemes.OrderBy(t => t.Name)]; - - if (darkThemes.Length > 0) - { - ImGui.TextDisabled("Dark Themes"); - ImGui.Separator(); - - foreach (ThemeRegistry.ThemeInfo theme in darkThemes) - { - bool isSelected = currentThemeName == theme.Name; - - if (RenderThemeMenuItemWithDialogSwatches(theme, theme.Name, isSelected)) - { - if (!isSelected && Apply(theme.Name)) - { - themeChanged = true; - } - } - } - - if (lightThemes.Length > 0) - { - ImGui.Separator(); - } - } - - if (lightThemes.Length > 0) - { - if (darkThemes.Length > 0) - { - ImGui.TextDisabled("Light Themes"); - ImGui.Separator(); - } - - foreach (ThemeRegistry.ThemeInfo theme in lightThemes) - { - bool isSelected = currentThemeName == theme.Name; - - if (RenderThemeMenuItemWithDialogSwatches(theme, theme.Name, isSelected)) - { - if (!isSelected && Apply(theme.Name)) - { - themeChanged = true; - } - } - } - } - - ImGui.EndMenu(); - } - - return themeChanged; - } - - #endregion - - #region Theme Menu Helpers - - /// - /// Renders a theme menu item with color preview swatches - /// - /// The theme to render. - /// The display name for the theme. - /// Whether this theme is currently selected. - /// True if the theme was clicked, false otherwise. -#pragma warning disable IDE0051 // Remove unused private members - preserved for reference - private static bool RenderThemeMenuItemWithSwatches(ThemeRegistry.ThemeInfo theme, string displayName, bool isSelected) - { - bool clicked = false; - - // Create a unique ID for this menu item - ImGui.PushID(theme.Name); - - try - { - // Get the theme's complete palette for color preview - IReadOnlyDictionary completePalette = GetCompletePalette(theme.CreateInstance()); - - // Define the colors we want to show: primary, alternate, medium neutral, low neutral - SemanticColorRequest[] colorRequests = [ - new SemanticColorRequest(SemanticMeaning.Primary, Priority.High), - new SemanticColorRequest(SemanticMeaning.Alternate, Priority.High), - new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Medium), - new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Low) - ]; - - // Use Selectable instead of MenuItem so we can draw custom content - if (ImGui.Selectable($"##theme_{theme.Name}", isSelected, ImGuiSelectableFlags.None)) - { - clicked = true; - } - - // Draw color swatches and theme name on top of the selectable - Vector2 itemMin = ImGui.GetItemRectMin(); - Vector2 itemMax = ImGui.GetItemRectMax(); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - float swatchSize = 12.0f; - float swatchSpacing = 2.0f; - float textOffset = (colorRequests.Length * (swatchSize + swatchSpacing)) + 8.0f; - - // Draw color swatches - for (int i = 0; i < colorRequests.Length; i++) - { - if (completePalette.TryGetValue(colorRequests[i], out PerceptualColor color)) - { - ImColor imColor = Color.FromPerceptualColor(color); - Vector2 swatchPos = new( - itemMin.X + 4.0f + (i * (swatchSize + swatchSpacing)), - itemMin.Y + ((itemMax.Y - itemMin.Y - swatchSize) * 0.5f) - ); - - // Draw swatch background (slightly larger for border effect) - drawList.AddRectFilled( - swatchPos - Vector2.One, - swatchPos + new Vector2(swatchSize + 1, swatchSize + 1), - ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 1.0f)) - ); - - // Draw color swatch - drawList.AddRectFilled( - swatchPos, - swatchPos + new Vector2(swatchSize, swatchSize), - ImGui.ColorConvertFloat4ToU32(imColor.Value) - ); - } - } - - // Draw theme name text - Vector2 textPos = new(itemMin.X + textOffset, itemMin.Y + ((itemMax.Y - itemMin.Y - ImGui.GetTextLineHeight()) * 0.5f)); - uint textColor = isSelected ? - ImGui.ColorConvertFloat4ToU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Text]) : - ImGui.ColorConvertFloat4ToU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Text]); - - drawList.AddText(textPos, textColor, displayName); - - // Add tooltip with theme description if hovered - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip($"{theme.Description}\n\nColors shown: Primary, Alternate, Neutral (Med), Neutral (Low)"); - } - } - catch (ArgumentException) - { - // Fallback to simple menu item if color extraction fails - clicked = ImGui.MenuItem(displayName, "", isSelected); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(theme.Description); - } - } - catch (InvalidOperationException) - { - // Fallback to simple menu item if color extraction fails - clicked = ImGui.MenuItem(displayName, "", isSelected); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(theme.Description); - } - } - catch (System.Reflection.TargetInvocationException) - { - // Fallback to simple menu item if reflection call fails - clicked = ImGui.MenuItem(displayName, "", isSelected); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(theme.Description); - } - } - finally - { - ImGui.PopID(); - } - - return clicked; - } -#pragma warning restore IDE0051 // Remove unused private members - - /// - /// Renders a theme menu item styled like a mini dialog window with title bar and content area. - /// - /// The theme to render. - /// The display name for the theme. - /// Whether this theme is currently selected. - /// True if the theme was clicked, false otherwise. - private static bool RenderThemeMenuItemWithDialogSwatches(ThemeRegistry.ThemeInfo theme, string displayName, bool isSelected) - { - bool clicked = false; - - // Create a unique ID for this menu item - ImGui.PushID(theme.Name); - - try - { - // Get the theme's complete palette for color preview - IReadOnlyDictionary completePalette = GetCompletePalette(theme.CreateInstance()); - - // Get primary color for title bar and surface color for background - ImColor primaryColor = Color.Palette.Basic.Blue; // Fallback - ImColor surfaceColor = Color.Palette.Neutral.Gray; // Fallback - - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Primary, Priority.High), out PerceptualColor primary)) - { - primaryColor = Color.FromPerceptualColor(primary); - } - - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Low), out PerceptualColor surface)) - { - surfaceColor = Color.FromPerceptualColor(surface); - } - else if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Medium), out PerceptualColor surfaceMed)) - { - surfaceColor = Color.FromPerceptualColor(surfaceMed); - } - - // Calculate required width for the dialog window - // This ensures the menu expands wide enough to show our custom dialog rendering - Vector2 textSize = ImGui.CalcTextSize(displayName); - float minDialogWidth = Math.Max(textSize.X + 16.0f, 140.0f); // Text width + padding, minimum 140px - float dialogHeight = 32.0f; // Height for the dialog window - Vector2 selectableSize = new(minDialogWidth, dialogHeight); - - // Use invisible selectable for interaction with proper sizing - if (ImGui.Selectable($"##theme_{theme.Name}", isSelected, ImGuiSelectableFlags.None, selectableSize)) - { - clicked = true; - } - - // Get item bounds for custom drawing - Vector2 itemMin = ImGui.GetItemRectMin(); - Vector2 itemMax = ImGui.GetItemRectMax(); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - float titleBarHeight = 8.0f; // Height of the dialog title bar - float margin = 2.0f; - - // Calculate dialog window bounds using the full selectable area - Vector2 dialogMin = new(itemMin.X + margin, itemMin.Y + margin); - Vector2 dialogMax = new(itemMax.X - margin, itemMax.Y - margin); - Vector2 titleBarMax = new(dialogMax.X, dialogMin.Y + titleBarHeight); - - // Draw dialog window shadow/border (slightly offset and darker) - Vector2 shadowOffset = new(1.0f, 1.0f); - drawList.AddRectFilled( - dialogMin + shadowOffset, - dialogMax + shadowOffset, - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 0.3f)), - 2.0f - ); - - // Draw main surface background - drawList.AddRectFilled( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(surfaceColor.Value), - 2.0f - ); - - // Draw primary color title bar - drawList.AddRectFilled( - dialogMin, - titleBarMax, - ImGui.ColorConvertFloat4ToU32(primaryColor.Value), - 2.0f, - ImDrawFlags.RoundCornersTop - ); - - // Add subtle inner glow for selected themes - if (isSelected) - { - // Inner glow - subtle light glow inside the dialog - drawList.AddRect( - dialogMin + Vector2.One, - dialogMax - Vector2.One, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.4f)), - 2.0f, - ImDrawFlags.None, - 1.0f - ); - } - - // Add hover effect - if (ImGui.IsItemHovered()) - { - drawList.AddRect( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.4f)), - 2.0f, - ImDrawFlags.None, - 1.0f - ); - } - - // Calculate contrasting text color for the surface - Vector4 surfaceVec = surfaceColor.Value; - float luminance = (0.299f * surfaceVec.X) + (0.587f * surfaceVec.Y) + (0.114f * surfaceVec.Z); - uint textColor = luminance > 0.5f ? - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)) : // Dark text on light surface - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Light text on dark surface - - // Draw theme name text over the surface area (below title bar) - Vector2 textPos = new( - dialogMin.X + 4.0f, - titleBarMax.Y + 2.0f - ); - - drawList.AddText(textPos, textColor, displayName); - - // Add tooltip with theme description if hovered - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip($"{theme.Description}\n\nDialog style: Primary title bar, surface background"); - } - } - catch (ArgumentException) - { - // Fallback to simple menu item if color extraction fails - clicked = ImGui.MenuItem(displayName, "", isSelected); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(theme.Description); - } - } - catch (InvalidOperationException) - { - // Fallback to simple menu item if color extraction fails - clicked = ImGui.MenuItem(displayName, "", isSelected); - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(theme.Description); - } - } - finally - { - ImGui.PopID(); - } - - return clicked; - } - - /// - /// Renders a family menu header styled like a mini dialog window with title bar and content area. - /// - /// The theme family name. - /// The theme to use for color extraction (typically first theme in family). - /// Whether any theme in this family is currently selected. - /// True if the menu should be opened, false otherwise. - private static bool RenderFamilyMenuHeader(string familyName, ThemeRegistry.ThemeInfo? representativeTheme, bool anyFamilyThemeSelected) - { - // Create a unique ID for this family header - ImGui.PushID($"Family_{familyName}"); - - bool menuOpened = false; - - try - { - // Get colors from the representative theme if available - ImColor primaryColor = Color.Palette.Basic.Blue; // Fallback - ImColor surfaceColor = Color.Palette.Neutral.Gray; // Fallback - - if (representativeTheme != null) - { - try - { - // Use the complete palette for efficient color extraction - IReadOnlyDictionary completePalette = GetCompletePalette(representativeTheme.CreateInstance()); - - // Get primary color for title bar - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Primary, Priority.High), out PerceptualColor primary)) - { - primaryColor = Color.FromPerceptualColor(primary); - } - - // Get surface color for background - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Low), out PerceptualColor surface)) - { - surfaceColor = Color.FromPerceptualColor(surface); - } - else if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Medium), out PerceptualColor surfaceMed)) - { - surfaceColor = Color.FromPerceptualColor(surfaceMed); - } - } - catch (ArgumentException) - { - // Use fallback colors if extraction fails - } - catch (InvalidOperationException) - { - // Use fallback colors if extraction fails - } - } - - // Use BeginMenu with transparent styling and draw our custom dialog over it - string displayText = familyName; // Don't add arrow, BeginMenu will handle it - - // Calculate proper width to match other menu items and account for arrow space - Vector2 textSize = ImGui.CalcTextSize(displayText); - float arrowWidth = ImGui.CalcTextSize(" â–ē").X; // Space needed for the arrow - float desiredWidth = Math.Max(textSize.X + arrowWidth + 16.0f, 180.0f); // Text + arrow + padding, minimum 180px - - // Push style to control menu item size - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, 4.0f)); // Ensure proper vertical spacing - - // Now use the standard BeginMenu with transparent styling to handle menu behavior - ImGui.PushStyleColor(ImGuiCol.Header, new Vector4(0, 0, 0, 0)); // Transparent - ImGui.PushStyleColor(ImGuiCol.HeaderHovered, new Vector4(1, 1, 1, 0.1f)); // Subtle hover - ImGui.PushStyleColor(ImGuiCol.HeaderActive, new Vector4(1, 1, 1, 0.2f)); // Subtle active - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 0)); // Hide text (we drew our own) - - // Use a dummy selectable to reserve the exact space we want, then use BeginMenu - Vector2 desiredSize = new(desiredWidth, 34.0f); // 34px height to match our dialog design - ImGui.Selectable($"##dummy_{familyName}", false, ImGuiSelectableFlags.Disabled, desiredSize); - Vector2 itemMin = ImGui.GetItemRectMin(); - Vector2 itemMax = ImGui.GetItemRectMax(); - - // Move cursor back to draw BeginMenu over the same space - ImGui.SetCursorScreenPos(itemMin); - menuOpened = ImGui.BeginMenu($"##menu_{familyName}"); - - // Draw our custom dialog using the reserved space - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - float titleBarHeight = 8.0f; // Height of the dialog title bar - float margin = 2.0f; - float arrowSpace = 20.0f; // Reserve space on right for arrow - - // Calculate dialog window bounds using reserved space, leaving room for arrow - Vector2 dialogMin = new(itemMin.X + margin, itemMin.Y + margin); - Vector2 dialogMax = new(itemMax.X - margin - arrowSpace, itemMax.Y - margin); - Vector2 titleBarMax = new(dialogMax.X, dialogMin.Y + titleBarHeight); - - // Draw dialog window shadow/border (slightly offset and darker) - Vector2 shadowOffset = new(1.0f, 1.0f); - drawList.AddRectFilled( - dialogMin + shadowOffset, - dialogMax + shadowOffset, - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 0.3f)), - 2.0f - ); - - // Draw main surface background - drawList.AddRectFilled( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(surfaceColor.Value), - 2.0f - ); - - // Draw primary color title bar - drawList.AddRectFilled( - dialogMin, - titleBarMax, - ImGui.ColorConvertFloat4ToU32(primaryColor.Value), - 2.0f, - ImDrawFlags.RoundCornersTop - ); - - // Add subtle inner glow if any family theme is selected - if (anyFamilyThemeSelected) - { - // Inner glow - subtle light glow inside the dialog - drawList.AddRect( - dialogMin + Vector2.One, - dialogMax - Vector2.One, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.3f)), - 2.0f, - ImDrawFlags.None, - 1.0f - ); - } - - // Calculate contrasting text color for the surface - Vector4 surfaceVec = surfaceColor.Value; - float luminance = (0.299f * surfaceVec.X) + (0.587f * surfaceVec.Y) + (0.114f * surfaceVec.Z); - uint textColor = luminance > 0.5f ? - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)) : // Dark text on light surface - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Light text on dark surface - - // Draw family name text over the surface area (below title bar) - Vector2 textPos = new( - dialogMin.X + 4.0f, - titleBarMax.Y + 2.0f - ); - - drawList.AddText(textPos, textColor, displayText); - - ImGui.PopStyleColor(4); - ImGui.PopStyleVar(); // Pop the ItemSpacing style - } - catch - { - // If there's an exception and menu wasn't opened, clean up the ID - if (!menuOpened) - { - ImGui.PopID(); - } - throw; - } - - // Only pop ID if menu is not opened - if opened, RenderFamilySubmenu will handle it - if (!menuOpened) - { - ImGui.PopID(); - } - - return menuOpened; - } - - /// - /// Renders a group header styled like a mini dialog window with title bar and content area. - /// - /// The group name (e.g., "Dark", "Light"). - /// The theme to use for color extraction (typically first theme in group). - /// Whether any theme in this group is currently selected. - private static void RenderGroupHeader(string groupName, ThemeRegistry.ThemeInfo representativeTheme, bool anyGroupThemeSelected) - { - // Create a unique ID for this group header - ImGui.PushID($"Group_{groupName}"); - - try - { - // Get colors from the representative theme - ImColor primaryColor = Color.Palette.Basic.Blue; // Fallback - ImColor surfaceColor = Color.Palette.Neutral.Gray; // Fallback - - try - { - // Use the complete palette for efficient color extraction - IReadOnlyDictionary completePalette = GetCompletePalette(representativeTheme.CreateInstance()); - - // Get primary color for title bar - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Primary, Priority.High), out PerceptualColor primary)) - { - primaryColor = Color.FromPerceptualColor(primary); - } - - // Get surface color for background - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Low), out PerceptualColor surface)) - { - surfaceColor = Color.FromPerceptualColor(surface); - } - else if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Medium), out PerceptualColor surfaceMed)) - { - surfaceColor = Color.FromPerceptualColor(surfaceMed); - } - } - catch (ArgumentException) - { - // Use fallback colors if extraction fails - } - catch (InvalidOperationException) - { - // Use fallback colors if extraction fails - } - - // Calculate required width for the dialog window - Vector2 textSize = ImGui.CalcTextSize(groupName); - float minDialogWidth = Math.Max(textSize.X + 16.0f, 120.0f); // Text width + padding, minimum 120px for groups - float dialogHeight = 24.0f; // Smaller height for group headers - Vector2 selectableSize = new(minDialogWidth, dialogHeight); - - // Use invisible selectable for proper sizing (non-interactive) - ImGui.Selectable($"##group_{groupName}", false, ImGuiSelectableFlags.Disabled, selectableSize); - - // Get item bounds for custom drawing - Vector2 itemMin = ImGui.GetItemRectMin(); - Vector2 itemMax = ImGui.GetItemRectMax(); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - float titleBarHeight = 6.0f; // Smaller title bar for group headers - float margin = 1.5f; - - // Calculate dialog window bounds using the full selectable area - Vector2 dialogMin = new(itemMin.X + margin, itemMin.Y + margin); - Vector2 dialogMax = new(itemMax.X - margin, itemMax.Y - margin); - Vector2 titleBarMax = new(dialogMax.X, dialogMin.Y + titleBarHeight); - - // Draw dialog window shadow/border (slightly offset and darker) - Vector2 shadowOffset = new(0.5f, 0.5f); - drawList.AddRectFilled( - dialogMin + shadowOffset, - dialogMax + shadowOffset, - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 0.2f)), - 1.5f - ); - - // Draw main surface background - drawList.AddRectFilled( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(surfaceColor.Value), - 1.5f - ); - - // Draw primary color title bar - drawList.AddRectFilled( - dialogMin, - titleBarMax, - ImGui.ColorConvertFloat4ToU32(primaryColor.Value), - 1.5f, - ImDrawFlags.RoundCornersTop - ); - - // Add subtle inner glow if any group theme is selected - if (anyGroupThemeSelected) - { - // Inner glow - subtle light glow inside the dialog - drawList.AddRect( - dialogMin + new Vector2(0.5f, 0.5f), - dialogMax - new Vector2(0.5f, 0.5f), - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.25f)), - 1.5f, - ImDrawFlags.None, - 0.8f - ); - } - - // Calculate contrasting text color for the surface - Vector4 surfaceVec = surfaceColor.Value; - float luminance = (0.299f * surfaceVec.X) + (0.587f * surfaceVec.Y) + (0.114f * surfaceVec.Z); - uint textColor = luminance > 0.5f ? - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)) : // Dark text on light surface - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Light text on dark surface - - // Draw group name text centered over the surface area (below title bar) - Vector2 textPos = new( - dialogMin.X + ((dialogMax.X - dialogMin.X - textSize.X) * 0.5f), // Centered horizontally - titleBarMax.Y + 1.0f // Small padding below title bar - ); - - drawList.AddText(textPos, textColor, groupName); - } - finally - { - ImGui.PopID(); - } - } - - #endregion - - #region Theme Discovery - - /// - /// Gets all available themes with their metadata. - /// - public static IReadOnlyList AllThemes => ThemeRegistry.AllThemes; - - /// - /// Gets all dark themes. - /// - public static IReadOnlyList DarkThemes => ThemeRegistry.DarkThemes; - - /// - /// Gets all light themes. - /// - public static IReadOnlyList LightThemes => ThemeRegistry.LightThemes; - - /// - /// Gets themes grouped by family. - /// - public static IReadOnlyDictionary> ThemesByFamily => ThemeRegistry.ThemesByFamily; - - /// - /// Gets all theme families. - /// - public static IReadOnlyList Families => ThemeRegistry.Families; - - /// - /// Finds a theme by name (case-insensitive). - /// - /// The theme name to search for. - /// The theme info if found, null otherwise. - public static ThemeRegistry.ThemeInfo? FindTheme(string name) => ThemeRegistry.FindTheme(name); - - /// - /// Gets all themes in a specific family. - /// - /// The family name. - /// Array of themes in the family. - public static IReadOnlyList GetThemesInFamily(string family) => ThemeRegistry.GetThemesInFamily(family); - - /// - /// Creates instances of all themes. - /// - /// Array of all theme instances. - public static IReadOnlyList CreateAllThemeInstances() => ThemeRegistry.CreateAllThemeInstances(); - - /// - /// Creates theme instances for a specific family. - /// - /// The family name. - /// Array of theme instances in the family. - public static IReadOnlyList CreateThemeInstancesInFamily(string family) => ThemeRegistry.CreateThemeInstancesInFamily(family); - - #endregion - - #region Complete Palette Access - - /// - /// Gets the complete palette for the current theme containing all possible semantic color combinations. - /// This provides every color that can be requested from the theme, useful for theme exploration and previews. - /// - /// A dictionary mapping every possible semantic color request to its assigned color, or null if no theme is active. - public static IReadOnlyDictionary? GetCurrentThemeCompletePalette() - { - ThemeRegistry.ThemeInfo? currentTheme = CurrentTheme; - if (currentTheme is null) - { - return null; - } - - return GetCompletePalette(currentTheme.CreateInstance()); - } - - /// - /// Gets the complete palette for a specific theme containing all possible semantic color combinations. - /// This provides every color that can be requested from the theme, useful for theme exploration and previews. - /// Uses the MakeCompletePalette API with efficient caching to avoid expensive recalculation. - /// - /// The semantic theme to generate the complete palette from. - /// A dictionary mapping every possible semantic color request to its assigned color. - public static IReadOnlyDictionary GetCompletePalette(ISemanticTheme theme) - { - ArgumentNullException.ThrowIfNull(theme); - - // Generate a cache key based on the theme - string cacheKey = GenerateThemeCacheKey(theme); - - // Check cache first - using (paletteCacheLock.EnterScope()) - { - if (paletteCache.TryGetValue(cacheKey, out IReadOnlyDictionary? cachedPalette)) - { - return cachedPalette; - } - } - - // Generate the palette - IReadOnlyDictionary palette = GeneratePaletteUncached(theme); - - // Cache the result - using (paletteCacheLock.EnterScope()) - { - // Limit cache size to prevent memory issues - if (paletteCache.Count >= 50) // Reasonable limit for theme count - { - // Remove oldest entries (simple FIFO) - string firstKey = paletteCache.Keys.First(); - paletteCache.Remove(firstKey); - } - - paletteCache[cacheKey] = palette; - } - - return palette; - } - - /// - /// Generates the complete palette without caching using the MakeCompletePalette API. - /// - /// The theme to generate the palette from. - /// The complete palette dictionary. - private static IReadOnlyDictionary GeneratePaletteUncached(ISemanticTheme theme) => - SemanticColorMapper.MakeCompletePalette(theme); - - /// - /// Generates a cache key for a theme based on its semantic mapping. - /// - /// The theme to generate a cache key for. - /// A unique cache key string. - private static string GenerateThemeCacheKey(ISemanticTheme theme) - { - // Create a hash based on the theme's semantic mapping content - // This ensures we get a new cache entry if the theme definition changes - System.Text.StringBuilder keyBuilder = new(); - - keyBuilder.Append(theme.GetType().FullName); - keyBuilder.Append('_'); - - // Add a simple hash of the semantic mappings - foreach (KeyValuePair> mapping in theme.SemanticMapping.OrderBy(kvp => kvp.Key)) - { - keyBuilder.Append(mapping.Key); - keyBuilder.Append(':'); - keyBuilder.Append(mapping.Value.Count); - keyBuilder.Append(';'); - } - - return keyBuilder.ToString(); - } - - /// - /// Clears the palette cache. Called when themes change. - /// - private static void ClearPaletteCache() - { - using (paletteCacheLock.EnterScope()) - { - paletteCache.Clear(); - } - } - - /// - /// Gets the complete palette for a theme by name. - /// - /// The name of the theme to get the palette for. - /// A dictionary mapping every possible semantic color request to its assigned color, or null if theme not found. - public static IReadOnlyDictionary? GetCompletePalette(string themeName) - { - ThemeRegistry.ThemeInfo? themeInfo = FindTheme(themeName); - if (themeInfo is null) - { - return null; - } - - return GetCompletePalette(themeInfo.CreateInstance()); - } - - /// - /// Gets all available semantic color requests for the current theme. - /// This is useful for discovering what colors are available without needing the actual color values. - /// - /// An array of all available semantic color requests, or empty array if no theme is active. - public static ImmutableArray GetCurrentThemeAvailableColorRequests() - { - IReadOnlyDictionary? palette = GetCurrentThemeCompletePalette(); - return palette?.Keys.ToImmutableArray() ?? []; - } - - /// - /// Tries to get a specific color from the current theme's complete palette. - /// This is more efficient than manually navigating semantic mappings. - /// - /// The semantic color request specifying the color to retrieve. - /// The retrieved color if found. - /// True if the color was found, false otherwise. - public static bool TryGetColor(SemanticColorRequest request, out PerceptualColor color) - { - IReadOnlyDictionary? palette = GetCurrentThemeCompletePalette(); - if (palette is not null && palette.TryGetValue(request, out color)) - { - return true; - } - - color = default; - return false; - } - - /// - /// Gets a specific color from the current theme's complete palette. - /// This is more efficient than manually navigating semantic mappings. - /// - /// The semantic color request specifying the color to retrieve. - /// The color if found, null otherwise. - public static PerceptualColor? GetColor(SemanticColorRequest request) => - TryGetColor(request, out PerceptualColor color) ? color : null; - - #endregion - - #region Scoped Theme Colors - - /// - /// Creates a scoped theme color that automatically reverts when disposed. - /// - /// The color to apply to the theme. - /// A scoped theme color instance. - public static ScopedThemeColor FromColor(ImColor color) => new(color, enabled: true); - - /// - /// Creates a scoped disabled theme color that automatically reverts when disposed. - /// - /// The color to apply to the theme. - /// A scoped theme color instance with disabled state. - public static ScopedThemeColor DisabledFromColor(ImColor color) => new(color, enabled: false); - - #endregion - - #region Theme Selector Dialog - - /// - /// Shows the theme browser modal. Call this to programmatically open the theme selector. - /// - /// The title for the theme browser modal. - /// Custom size for the modal window. Default is 900x650 to accommodate wider theme cards. - public static void ShowThemeSelector(string title = "🎨 Theme Browser", Vector2? customSize = null) - { - themeBrowser.Open( - title, - onThemeSelected: themeName => { /* Theme was already applied in ThemeBrowser */ }, - onDefaultRequested: () => { /* Default was already applied in ThemeBrowser */ }, - customSize - ); - } - - /// - /// Hides the theme selector dialog. This is kept for API compatibility but the modal handles its own state. - /// - public static void HideThemeSelector() { /* Modal handles its own close state, so this is mainly for API compatibility */ } - - /// - /// Renders the theme browser modal if it's currently open. - /// This should be called in your main render loop to display the theme browser. - /// - /// True if a theme was changed during modal interaction, false otherwise. - public static bool RenderThemeSelector() => themeBrowser.ShowIfOpen(); - - /// - /// Renders the theme browser modal if it's currently open. - /// This overload is kept for backward compatibility. - /// - /// The title for the theme browser modal (ignored - kept for compatibility). - /// The size for the theme browser modal (ignored - kept for compatibility). - /// True if a theme was changed during modal interaction, false otherwise. - public static bool RenderThemeSelector(string windowTitle, Vector2? windowSize = null) - { - // Parameters are intentionally unused - kept for backward compatibility - _ = windowTitle; - _ = windowSize; - return themeBrowser.ShowIfOpen(); - } - - /// - /// Renders a theme menu that opens the theme browser modal instead of using dropdown menus. - /// This provides a better user experience than the traditional RenderMenu() method. - /// - /// The label for the theme submenu (default: "Theme") - /// True if a theme was selected and changed, false otherwise. - public static bool RenderThemeSelectorMenu(string menuLabel = "Theme") - { - bool themeChanged = false; - - if (ImGui.BeginMenu(menuLabel)) - { - if (ImGui.MenuItem("Browse Themes...")) - { - ShowThemeSelector(); - } - - ImGui.Separator(); - - // Quick reset option - if (ImGui.MenuItem("Reset to Default", "", currentThemeName is null)) - { - if (currentThemeName is not null) - { - ResetToDefault(); - themeChanged = true; - } - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Reset to default ImGui styling with no theme applied"); - } - - // Show current theme info if any - if (currentThemeName is not null) - { - ImGui.Separator(); - ImGui.TextDisabled($"Current: {currentThemeName}"); - ThemeRegistry.ThemeInfo? currentTheme = FindTheme(currentThemeName); - if (currentTheme is not null) - { - ImGui.TextDisabled($"({(currentTheme.IsDark ? "Dark" : "Light")})"); - } - } - - ImGui.EndMenu(); - } - - // Check if any theme changes occurred through the modal browser - // The modal handles theme application internally, so we check for changes here - bool modalThemeChanged = RenderThemeSelector(); - - return themeChanged || modalThemeChanged; - } - - #endregion -} diff --git a/ImGui.Styler/ThemeBrowser.cs b/ImGui.Styler/ThemeBrowser.cs deleted file mode 100644 index 9a5b0e8..0000000 --- a/ImGui.Styler/ThemeBrowser.cs +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Hexa.NET.ImGui; -using ktsu.ThemeProvider; -using ktsu.ImGui.Popups; - -/// -/// A modal theme browser that allows users to select themes from a visual grid. -/// -public class ThemeBrowser -{ - /// - /// The underlying modal instance for managing popup behavior using ktsu.ImGuiPopups. - /// - private readonly ImGuiPopups.Modal modal = new(); - - /// - /// The selected theme family filter index. - /// - private int selectedFamilyFilter; - - /// - /// The action to invoke when a theme is selected. - /// - private Action? onThemeSelected; - - /// - /// The action to invoke when the default theme is requested. - /// - private Action? onDefaultRequested; - - /// - /// Tracks whether a theme change occurred during the current modal session. - /// - private bool themeChanged; - - /// - /// Opens the theme browser modal. - /// - /// The title of the modal window. - /// Action to invoke when a theme is selected. Parameter is the theme name. - /// Action to invoke when default theme is requested. - /// Custom size of the modal. If not specified, uses a default size. - public void Open(string title = "🎨 Theme Browser", Action? onThemeSelected = null, Action? onDefaultRequested = null, Vector2? customSize = null) - { - this.onThemeSelected = onThemeSelected; - this.onDefaultRequested = onDefaultRequested; - themeChanged = false; // Reset theme change tracking for this modal session - Vector2 size = customSize ?? new Vector2(900, 650); // Increased width to accommodate wider theme cards - modal.Open(title, ShowContent, size); - } - - /// - /// Shows the theme browser modal if it's open and returns whether a theme was changed. - /// - /// True if a theme was changed during modal interaction, false otherwise. - public bool ShowIfOpen() - { - bool wasOpen = modal.WasOpen; - bool isOpen = modal.ShowIfOpen(); - - // If the modal was just closed and a theme was changed, return true and reset the flag - if (wasOpen && !isOpen && themeChanged) - { - themeChanged = false; - return true; - } - - // If modal is still open but theme was changed this frame, return true (but don't reset flag yet) - return themeChanged; - } - - /// - /// Shows the content of the theme browser modal. - /// - private void ShowContent() - { - // Header with current theme info - ImGui.Text("Choose a theme from the gallery below:"); - if (Theme.CurrentThemeName is not null) - { - ImGui.SameLine(); - using (Text.Color.Success()) - { - ImGui.Text($"Current: {Theme.CurrentThemeName}"); - } - } - else - { - ImGui.SameLine(); - using (Text.Color.Info()) - { - ImGui.Text("Current: Default"); - } - } - - ImGui.Separator(); - - // Theme family filter - ImGui.Text("Filter by Family:"); - ImGui.SameLine(); - - // Create family list for filtering - List familyList = ["All Themes", .. Theme.Families.OrderBy(f => f)]; - - if (ImGui.Combo("##FamilyFilter", ref selectedFamilyFilter, [.. familyList], familyList.Count)) - { - // Family filter changed - could add logic here if needed - } - - ImGui.Separator(); - - // Get filtered themes - IEnumerable filteredThemes = selectedFamilyFilter == 0 - ? Theme.AllThemes - : Theme.AllThemes.Where(t => t.Family == familyList[selectedFamilyFilter]); - - List themesToShow = [.. filteredThemes.OrderBy(t => t.Family).ThenBy(t => t.Name)]; - - // Theme count info - ImGui.Text($"Themes ({themesToShow.Count}):"); - - // Theme grid in a scrollable area - Vector2 childSize = new(0, ImGui.GetContentRegionAvail().Y - 60); // Leave space for buttons - ImGui.BeginChild("ThemeGridScrollArea", childSize, ImGuiChildFlags.Borders); - - // Use ThemeCard.RenderGrid with appropriate sizing for the modal - Vector2 cardSize = new(200, 90); // Increased width to accommodate longer theme names - ThemeCard.RenderGrid(themesToShow, selectedTheme => - { - // Apply the selected theme - if (Theme.Apply(selectedTheme.Name)) - { - themeChanged = true; // Set flag when theme is successfully applied - onThemeSelected?.Invoke(selectedTheme.Name); - //ImGui.CloseCurrentPopup(); - } - }, cardSize); - - ImGui.EndChild(); - - ImGui.NewLine(); - - // Modal buttons - float buttonWidth = 100.0f; - float totalButtonsWidth = (buttonWidth * 2) + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - totalButtonsWidth) * 0.5f); - - // Reset button - using (Theme.FromColor(Color.Palette.Semantic.Warning)) - { - if (ImGui.Button("Reset", new Vector2(buttonWidth, 0))) - { - Theme.ResetToDefault(); - themeChanged = true; // Set flag when default is applied - onDefaultRequested?.Invoke(); - //ImGui.CloseCurrentPopup(); - } - } - - ImGui.SameLine(); - - // Close button - if (ImGui.Button("Close", new Vector2(buttonWidth, 0))) - { - ImGui.CloseCurrentPopup(); - } - } -} diff --git a/ImGui.Styler/ThemeCard.cs b/ImGui.Styler/ThemeCard.cs deleted file mode 100644 index 9e0758e..0000000 --- a/ImGui.Styler/ThemeCard.cs +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Styler; - -using System; -using System.Collections.Generic; -using System.Numerics; -using Hexa.NET.ImGui; -using ktsu.ThemeProvider; - -/// -/// Provides theme preview card widgets for displaying theme information in a dialog window style. -/// -public static class ThemeCard -{ - /// - /// Renders a theme preview card styled like a mini dialog window with title bar and content area. - /// - /// The theme to render. - /// The size of the card. If not specified, uses a default size. - /// Whether this theme is currently selected. - /// True if the theme card was clicked, false otherwise. - public static bool Render(ThemeRegistry.ThemeInfo theme, Vector2? size = null, bool? isSelected = null) => - Render(theme, theme?.Name ?? string.Empty, size, isSelected); - - /// - /// Renders a theme preview card styled like a mini dialog window with title bar and content area. - /// This version uses a callback delegate to report theme selection. - /// - /// The theme to render. - /// Callback invoked when the theme is selected. - /// The size of the card. If not specified, uses a default size. - /// Whether this theme is currently selected. - public static void Render(ThemeRegistry.ThemeInfo theme, Action onThemeSelected, Vector2? size = null, bool? isSelected = null) => - Render(theme, theme?.Name ?? string.Empty, onThemeSelected, size, isSelected); - - /// - /// Renders a theme preview card styled like a mini dialog window with title bar and content area. - /// This version uses a callback delegate to report theme selection. - /// - /// The theme to render. - /// The display name for the theme (shown in the card). - /// Callback invoked when the theme is selected. - /// The size of the card. If not specified, uses a default size. - /// Whether this theme is currently selected. If not specified, compares against current theme. - public static void Render(ThemeRegistry.ThemeInfo theme, string displayName, Action onThemeSelected, Vector2? size = null, bool? isSelected = null) - { - ArgumentNullException.ThrowIfNull(theme); - ArgumentNullException.ThrowIfNull(displayName); - ArgumentNullException.ThrowIfNull(onThemeSelected); - - // Use the existing Render method and handle the click result - if (Render(theme, displayName, size, isSelected)) - { - onThemeSelected(theme); - } - } - - /// - /// Renders a theme preview card styled like a mini dialog window with title bar and content area. - /// - /// The theme to render. - /// The display name for the theme (shown in the card). - /// The size of the card. If not specified, uses a default size. - /// Whether this theme is currently selected. If not specified, compares against current theme. - /// True if the theme card was clicked, false otherwise. - public static bool Render(ThemeRegistry.ThemeInfo theme, string displayName, Vector2? size = null, bool? isSelected = null) - { - ArgumentNullException.ThrowIfNull(theme); - ArgumentNullException.ThrowIfNull(displayName); - - bool clicked = false; - - // Create a unique ID for this card - ImGui.PushID($"ThemeCard_{theme.Name}"); - - try - { - // Determine if this theme is selected - bool isCurrentTheme = isSelected ?? (Theme.CurrentThemeName == theme.Name); - - // Use default size if not specified - Vector2 cardSize = size ?? new Vector2(180, 70); - - // Get colors for dialog window style from complete palette - ImColor primaryColor = Color.Palette.Basic.Blue; // Fallback - ImColor surfaceColor = Color.Palette.Neutral.Gray; // Fallback - ImColor textColor = Color.Palette.Neutral.White; // Fallback - IReadOnlyDictionary? completePalette = null; - - try - { - // Use the complete palette for efficient color extraction - completePalette = Theme.GetCompletePalette(theme.CreateInstance()); - - // Get primary color for title bar - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Primary, Priority.High), out PerceptualColor primary)) - { - primaryColor = Color.FromPerceptualColor(primary); - } - - // Get surface color for background - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Low), out PerceptualColor surface)) - { - surfaceColor = Color.FromPerceptualColor(surface); - } - else if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.Medium), out PerceptualColor surfaceMed)) - { - surfaceColor = Color.FromPerceptualColor(surfaceMed); - } - - // Get highest priority neutral for text - if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.VeryHigh), out PerceptualColor textVeryHigh)) - { - textColor = Color.FromPerceptualColor(textVeryHigh); - } - else if (completePalette.TryGetValue(new SemanticColorRequest(SemanticMeaning.Neutral, Priority.High), out PerceptualColor textHigh)) - { - textColor = Color.FromPerceptualColor(textHigh); - } - } - catch (ArgumentException) - { - // Use fallback colors if extraction fails - } - catch (InvalidOperationException) - { - // Use fallback colors if extraction fails - } - - // Use invisible button for interaction - clicked = ImGui.InvisibleButton($"##card_{theme.Name}", cardSize); - - // Get button bounds for custom drawing - Vector2 cardMin = ImGui.GetItemRectMin(); - Vector2 cardMax = ImGui.GetItemRectMax(); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - bool isHovered = ImGui.IsItemHovered(); - bool isActive = ImGui.IsItemActive(); - - float titleBarHeight = 16.0f; // Height of the dialog title bar - float margin = 3.0f; - - // Calculate dialog window bounds (with margins) - Vector2 dialogMin = new(cardMin.X + margin, cardMin.Y + margin); - Vector2 dialogMax = new(cardMax.X - margin, cardMax.Y - margin); - Vector2 titleBarMax = new(dialogMax.X, dialogMin.Y + titleBarHeight); - - // Draw enhanced shadow for selected themes - Vector2 shadowOffset = isCurrentTheme ? new(3.0f, 3.0f) : new(2.0f, 2.0f); - float shadowOpacity = isCurrentTheme ? 0.4f : 0.2f; - drawList.AddRectFilled( - dialogMin + shadowOffset, - dialogMax + shadowOffset, - ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.0f, 0.0f, shadowOpacity)), - 3.0f - ); - - // Draw main surface background - drawList.AddRectFilled( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(surfaceColor.Value), - 3.0f - ); - - // Draw primary color title bar - drawList.AddRectFilled( - dialogMin, - titleBarMax, - ImGui.ColorConvertFloat4ToU32(primaryColor.Value), - 3.0f, - ImDrawFlags.RoundCornersTop - ); - - // Add subtle inner glow for selected themes - if (isCurrentTheme) - { - // Inner glow - subtle white glow inside the card - drawList.AddRect( - dialogMin + Vector2.One, - dialogMax - Vector2.One, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.3f)), - 2.5f, - ImDrawFlags.None, - 1.0f - ); - - // Secondary inner glow for more prominence - drawList.AddRect( - dialogMin + new Vector2(2.0f, 2.0f), - dialogMax - new Vector2(2.0f, 2.0f), - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.15f)), - 2.0f, - ImDrawFlags.None, - 0.5f - ); - } - - // Add hover effect - if (isHovered) - { - drawList.AddRect( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.6f)), - 3.0f, - ImDrawFlags.None, - 1.5f - ); - } - - // Add active (pressed) effect - if (isActive) - { - drawList.AddRectFilled( - dialogMin, - dialogMax, - ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 1.0f, 0.1f)), - 3.0f - ); - } - - // Draw theme name centered in content area (below title bar) - // Calculate display text (remove checkmark to avoid layout jumping) - string displayText = displayName; - Vector2 textSize = ImGui.CalcTextSize(displayText); - - // Calculate content area bounds (below title bar) - float contentHeight = dialogMax.Y - titleBarMax.Y; - - Vector2 textPos = new( - dialogMin.X + ((dialogMax.X - dialogMin.X - textSize.X) * 0.5f), // Centered horizontally - titleBarMax.Y + ((contentHeight - textSize.Y) * 0.5f) - 4.0f // Centered vertically but moved up 4px for balance - ); - - drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(textColor.Value), displayText); - - // Add semantic color swatches in bottom right corner - DrawSemanticSwatches(drawList, completePalette, dialogMax, margin); - - // Add tooltip with theme description if hovered - if (isHovered) - { - ImGui.SetTooltip($"{theme.Description}\n\nFamily: {theme.Family}\nType: {(theme.IsDark ? "Dark" : "Light")}\n\nColor swatches show: Primary, Alternate, Success, Warning, Error\n\nClick to select this theme"); - } - } - finally - { - ImGui.PopID(); - } - - return clicked; - } - - /// - /// Renders a grid of theme preview cards. - /// - /// The themes to display in the grid. - /// Size of each card. If not specified, uses default size. - /// Number of cards per row. If not specified, calculates based on available width. - /// The theme that was clicked, or null if no theme was clicked. - public static ThemeRegistry.ThemeInfo? RenderGrid(IEnumerable themes, Vector2? cardSize = null, int? columnsPerRow = null) - { - ArgumentNullException.ThrowIfNull(themes); - - Vector2 size = cardSize ?? new Vector2(180, 70); - int columns = columnsPerRow ?? Math.Max(1, (int)(ImGui.GetContentRegionAvail().X / (size.X + 10))); // 10px spacing - - ImGui.Columns(columns, "ThemeCardGrid", false); - - foreach (ThemeRegistry.ThemeInfo theme in themes) - { - if (Render(theme, size)) - { - ImGui.Columns(1); // Reset columns - return theme; - } - - ImGui.NextColumn(); - } - - ImGui.Columns(1); // Reset columns - return null; - } - - /// - /// Renders a grid of theme preview cards using a callback delegate for theme selection. - /// - /// The themes to display in the grid. - /// Callback invoked when any theme is selected. - /// Size of each card. If not specified, uses default size. - /// Number of cards per row. If not specified, calculates based on available width. - public static void RenderGrid(IEnumerable themes, Action onThemeSelected, Vector2? cardSize = null, int? columnsPerRow = null) - { - ArgumentNullException.ThrowIfNull(themes); - ArgumentNullException.ThrowIfNull(onThemeSelected); - - Vector2 size = cardSize ?? new Vector2(180, 70); - int columns = columnsPerRow ?? Math.Max(1, (int)(ImGui.GetContentRegionAvail().X / (size.X + 10))); // 10px spacing - - ImGui.Columns(columns, "ThemeCardGrid", false); - - foreach (ThemeRegistry.ThemeInfo theme in themes) - { - Render(theme, onThemeSelected, size); - ImGui.NextColumn(); - } - - ImGui.Columns(1); // Reset columns - } - - /// - /// Draws small semantic color swatches in the bottom right corner of a theme card. - /// - /// The ImGui draw list to draw on. - /// The complete color palette for the theme. - /// The bottom-right corner of the dialog area. - /// The margin from the dialog edge. - private static void DrawSemanticSwatches(ImDrawListPtr drawList, IReadOnlyDictionary? completePalette, Vector2 dialogMax, float margin) - { - // Skip drawing swatches if palette is not available - if (completePalette is null) - { - return; - } - - // Define the semantic meanings to show as swatches (in order) - SemanticMeaning[] swatchMeanings = [ - SemanticMeaning.Primary, - SemanticMeaning.Alternate, - SemanticMeaning.Success, - SemanticMeaning.Warning, - SemanticMeaning.Error - ]; - - const float swatchSize = 8.0f; // Small square size (increased from 6.0f) - const float swatchSpacing = 2.0f; // Spacing between squares (changed to whole number) - const float swatchPadding = 3.0f; // Padding from dialog edge (already whole number) - - // Calculate starting position (bottom-right, working left) - float totalWidth = (swatchMeanings.Length * swatchSize) + ((swatchMeanings.Length - 1) * swatchSpacing); - Vector2 startPos = new( - dialogMax.X - margin - swatchPadding - totalWidth, - dialogMax.Y - margin - swatchPadding - swatchSize - ); - - // Draw each semantic color swatch - for (int i = 0; i < swatchMeanings.Length; i++) - { - SemanticColorRequest colorRequest = new(swatchMeanings[i], Priority.High); - - // Try to get the color from the complete palette - if (completePalette.TryGetValue(colorRequest, out PerceptualColor semanticColor)) - { - ImColor swatchColor = Color.FromPerceptualColor(semanticColor); - - Vector2 swatchMin = new( - startPos.X + (i * (swatchSize + swatchSpacing)), - startPos.Y - ); - Vector2 swatchMax = new( - swatchMin.X + swatchSize, - swatchMin.Y + swatchSize - ); - - // Draw the color swatch as a flat square - drawList.AddRectFilled(swatchMin, swatchMax, ImGui.ColorConvertFloat4ToU32(swatchColor.Value)); - } - } - } -} diff --git a/ImGui.Widgets/ColorIndicator.cs b/ImGui.Widgets/ColorIndicator.cs deleted file mode 100644 index 7ec575f..0000000 --- a/ImGui.Widgets/ColorIndicator.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Numerics; - -using Hexa.NET.ImGui; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Displays a colored square. - /// - /// The color to be displayed when enabled. - /// A boolean indicating whether the ColorIndicator is enabled. - public static void ColorIndicator(ImColor color, bool enabled) => ColorIndicatorImpl.Show(color, enabled); - - internal static class ColorIndicatorImpl - { - public static void Show(ImColor color, bool enabled) - { - float frameHeight = ImGui.GetFrameHeight(); - ImGui.Dummy(new Vector2(frameHeight, frameHeight)); - Vector2 dummyRectMin = ImGui.GetItemRectMin(); - Vector2 dummyRectMax = ImGui.GetItemRectMax(); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - uint colorToShow = enabled ? ImGui.GetColorU32(color.Value) : ImGui.GetColorU32(ImGuiCol.FrameBg); - drawList.AddRectFilled(dummyRectMin, dummyRectMax, colorToShow); - } - } -} diff --git a/ImGui.Widgets/Combo.cs b/ImGui.Widgets/Combo.cs deleted file mode 100644 index 9be62d3..0000000 --- a/ImGui.Widgets/Combo.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Collections.ObjectModel; -using System.Globalization; -using Hexa.NET.ImGui; - -using ktsu.Semantics.Strings; - -public static partial class ImGuiWidgets -{ - /// - /// An ImGui.Combo implementation that works with Enums. - /// - /// Type of the Enum. - /// Label for display and id. - /// The currently selected value. - /// If a combo value was selected. - public static bool Combo(string label, ref TEnum selectedValue) where TEnum : Enum - { - Array possibleValues = Enum.GetValues(typeof(TEnum)); - int currentIndex = Array.IndexOf(possibleValues, selectedValue); - string[] possibleValuesNames = Enum.GetNames(typeof(TEnum)); - if (ImGui.Combo(label, ref currentIndex, possibleValuesNames, possibleValuesNames.Length)) - { - selectedValue = (TEnum)possibleValues.GetValue(currentIndex)!; - return true; - } - - return false; - } - - /// - /// An ImGui.Combo implementation that works with IStrings. - /// - /// Type of the StrongString - /// Label for display and id. - /// The currently selected value. - /// The collection of possible values. - /// If a combo value was selected. - public static bool Combo(string label, ref TString selectedValue, Collection possibleValues) where TString : ISemanticString - { - ArgumentNullException.ThrowIfNull(possibleValues); - - int currentIndex = possibleValues.IndexOf(selectedValue); - string[] possibleValuesNames = [.. possibleValues.Select(e => e.ToString(CultureInfo.InvariantCulture))]; - if (ImGui.Combo(label, ref currentIndex, possibleValuesNames, possibleValuesNames.Length)) - { - selectedValue = possibleValues[currentIndex]; - return true; - } - - return false; - } - - /// - /// An ImGui.Combo implementation that works with strings. - /// - /// Label for display and id. - /// The currently selected value. - /// The collection of possible values. - /// If a combo value was selected. - public static bool Combo(string label, ref string selectedValue, Collection possibleValues) - { - ArgumentNullException.ThrowIfNull(possibleValues); - - int currentIndex = possibleValues.IndexOf(selectedValue); - string[] possibleValuesNames = [.. possibleValues]; - if (ImGui.Combo(label, ref currentIndex, possibleValuesNames, possibleValuesNames.Length)) - { - selectedValue = possibleValues[currentIndex]; - return true; - } - - return false; - } -} diff --git a/ImGui.Widgets/CompatibilitySuppressions.xml b/ImGui.Widgets/CompatibilitySuppressions.xml deleted file mode 100644 index df05015..0000000 --- a/ImGui.Widgets/CompatibilitySuppressions.xml +++ /dev/null @@ -1,52 +0,0 @@ -īģŋ - - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureGuardAttribute - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0001 - T:System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0001 - T:System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0001 - T:System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0001 - T:System.Runtime.CompilerServices.ParamCollectionAttribute - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0001 - T:System.Threading.Lock - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_UrlFormat:[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - - CP0014 - M:System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_UrlFormat(System.String):[T:System.Runtime.CompilerServices.CompilerGeneratedAttribute] - lib/net8.0/ktsu.ImGui.Widgets.dll - lib/net9.0/ktsu.ImGui.Widgets.dll - - \ No newline at end of file diff --git a/ImGui.Widgets/DESCRIPTION.md b/ImGui.Widgets/DESCRIPTION.md deleted file mode 100644 index 2b61e9f..0000000 --- a/ImGui.Widgets/DESCRIPTION.md +++ /dev/null @@ -1 +0,0 @@ -A library of custom widgets using ImGui.NET and utilities to enhance ImGui-based applications. diff --git a/ImGui.Widgets/DividerContainer.cs b/ImGui.Widgets/DividerContainer.cs deleted file mode 100644 index 90a9a3e..0000000 --- a/ImGui.Widgets/DividerContainer.cs +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Collections.ObjectModel; -using System.Drawing; -using System.Numerics; - -using Extensions; - -using Hexa.NET.ImGui; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// An enum to specify the layout direction of the divider container. - /// - public enum DividerLayout - { - /// - /// The container will be laid out in columns. - /// - Columns, - /// - /// The container will be laid out in rows. - /// - Rows, - } - - /// - /// A container that can be divided into dragable zones. - /// Useful for creating resizable layouts. - /// Containers can be nested to create complex layouts. - /// - /// - /// Create a new divider container with a callback for when the container is resized and a specified layout direction. - /// - /// The ID of the container. - /// A callback for when the container is resized. - /// The layout direction of the container. - /// The zones to add to the container. - public class DividerContainer(string id, Action? onResized, DividerLayout layout, IEnumerable zones) - { - /// - /// An ID for the container. - /// - public string Id { get; init; } = id; - private int DragIndex { get; set; } = -1; - private List Zones { get; init; } = [.. zones]; - - /// - /// Create a new divider container with a callback for when the container is resized and a specified layout direction. - /// - /// The ID of the container. - /// A callback for when the container is resized. - /// The layout direction of the container. - public DividerContainer(string id, Action? onResized, DividerLayout layout) - : this(id, onResized, layout, []) - { - } - - /// - /// Create a new divider container with the default layout direction of columns. - /// - /// The ID of the container. - public DividerContainer(string id) - : this(id, null, DividerLayout.Columns) - { - } - - /// - /// Create a new divider container with a specified layout direction. - /// - /// The ID of the container. - /// The layout direction of the container. - public DividerContainer(string id, DividerLayout layout) - : this(id, null, layout) - { - } - - /// - /// Create a new divider container with a callback for when the container is resized and the default layout direction of columns. - /// - /// The ID of the container. - /// A callback for when the container is resized. - public DividerContainer(string id, Action? onResized) - : this(id, onResized, DividerLayout.Columns) - { - } - - /// - /// Tick the container to update and draw its contents. - /// - /// The delta time since the last tick. - /// Thrown if the layout direction is not supported. - public void Tick(float dt) - { - ImGuiStylePtr style = ImGui.GetStyle(); - Vector2 windowPadding = style.WindowPadding; - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - Vector2 containerSize = ImGui.GetWindowSize() - (windowPadding * 2.0f); - - Vector2 layoutMask = layout switch - { - DividerLayout.Columns => new Vector2(1, 0), - DividerLayout.Rows => new Vector2(0, 1), - _ => throw new NotImplementedException(), - }; - - Vector2 layoutMaskInverse = layout switch - { - DividerLayout.Columns => new Vector2(0, 1), - DividerLayout.Rows => new Vector2(1, 0), - _ => throw new NotImplementedException(), - }; - - Vector2 windowPos = ImGui.GetWindowPos(); - Vector2 advance = windowPos + windowPadding; - - ImGui.SetNextWindowPos(advance); - ImGui.BeginChild(Id, containerSize, ImGuiChildFlags.None, ImGuiWindowFlags.NoSavedSettings); - - foreach (DividerZone z in Zones) - { - Vector2 zoneSize = CalculateZoneSize(z, windowPadding, containerSize, layoutMask, layoutMaskInverse); - ImGui.SetNextWindowPos(advance); - ImGui.BeginChild(z.Id, zoneSize, ImGuiChildFlags.Borders, ImGuiWindowFlags.NoSavedSettings); - z.Tick(dt); - ImGui.EndChild(); - - advance += CalculateAdvance(z, windowPadding, containerSize, layoutMask); - } - - ImGui.EndChild(); - - //render the handles last otherwise they'll be covered by the other zones windows and wont receive hover events - - //reset the advance to the top left of the container - advance = windowPos + windowPadding; - float resize = 0; - Vector2 mousePos = ImGui.GetMousePos(); - bool resetSize = false; - foreach ((DividerZone z, int i) in Zones.WithIndex()) - { - //draw the grab handle if we're not the last zone - if (z != Zones.Last()) - { - Vector2 zoneSize = CalculateZoneSize(z, windowPadding, containerSize, layoutMask, layoutMaskInverse); - Vector2 lineA = advance + (zoneSize * layoutMask) + (windowPadding * 0.5f * layoutMask); - Vector2 lineB = lineA + (zoneSize * layoutMaskInverse); - float lineWidth = style.WindowPadding.X * 0.5f; - float grabWidth = style.WindowPadding.X * 2; - Vector2 grabBox = new Vector2(grabWidth, grabWidth) * 0.5f; - Vector2 grabMin = lineA - (grabBox * layoutMask); - Vector2 grabMax = lineB + (grabBox * layoutMask); - Vector2 grabSize = grabMax - grabMin; - RectangleF handleRect = new(grabMin.X, grabMin.Y, grabSize.X, grabSize.Y); - bool handleHovered = handleRect.Contains(mousePos.X, mousePos.Y); - bool mouseClickedThisFrame = ImGui.IsMouseClicked(ImGuiMouseButton.Left); - bool handleClicked = handleHovered && mouseClickedThisFrame; - bool handleDoubleClicked = handleHovered && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left); - - if (handleClicked) - { - DragIndex = i; - } - - if (handleDoubleClicked) - { - resetSize = true; - } - else if (DragIndex == i) - { - if (ImGui.IsMouseDown(ImGuiMouseButton.Left)) - { - Vector2 mousePosLocal = mousePos - advance; - - DividerZone first = Zones.First(); - DividerZone last = Zones.Last(); - if (first != last && z != first) - { - mousePosLocal += windowPadding * 0.5f * layoutMask; - } - - float requestedSize = layout switch - { - DividerLayout.Columns => mousePosLocal.X / containerSize.X, - DividerLayout.Rows => mousePosLocal.Y / containerSize.Y, - _ => throw new NotImplementedException(), - }; - resize = Math.Clamp(requestedSize, 0.1f, 0.9f); - } - else - { - DragIndex = -1; - } - } - - Vector4 lineColor = DragIndex == i ? new Vector4(1, 1, 1, 0.7f) : handleHovered ? new Vector4(1, 1, 1, 0.5f) : new Vector4(1, 1, 1, 0.3f); - //drawList.AddRectFilled(grabMin, grabMax, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.3f))); - drawList.AddLine(lineA, lineB, ImGui.ColorConvertFloat4ToU32(lineColor), lineWidth); - - if (handleHovered || DragIndex == i) - { - ImGui.SetMouseCursor(layout switch - { - DividerLayout.Columns => ImGuiMouseCursor.ResizeEw, - DividerLayout.Rows => ImGuiMouseCursor.ResizeNs, - _ => throw new NotImplementedException(), - }); - } - } - - advance += CalculateAdvance(z, windowPadding, containerSize, layoutMask); - } - - //do the actual resize at the end of the tick so that we don't mess with the dimensions of the layout mid rendering - if (DragIndex > -1) - { - if (resetSize) - { - resize = Zones[DragIndex].InitialSize; - } - - DividerZone resizedZone = Zones[DragIndex]; - DividerZone neighbourZone = Zones[DragIndex + 1]; - float combinedSize = resizedZone.Size + neighbourZone.Size; - float maxSize = combinedSize - 0.1f; - resize = Math.Clamp(resize, 0.1f, maxSize); - bool sizeDidChange = resizedZone.Size != resize; - resizedZone.Size = resize; - neighbourZone.Size = combinedSize - resize; - if (sizeDidChange) - { - onResized?.Invoke(this); - } - - if (resetSize) - { - DragIndex = -1; - } - } - } - - private Vector2 CalculateZoneSize(DividerZone z, Vector2 windowPadding, Vector2 containerSize, Vector2 layoutMask, Vector2 layoutMaskInverse) - { - Vector2 zoneSize = (containerSize * z.Size * layoutMask) + (containerSize * layoutMaskInverse); - - DividerZone first = Zones.First(); - DividerZone last = Zones.Last(); - if (first != last) - { - if (z == first || z == last) - { - zoneSize -= windowPadding * 0.5f * layoutMask; - } - else - { - zoneSize -= windowPadding * layoutMask; - } - } - - return new Vector2((float)Math.Floor(zoneSize.X), (float)Math.Floor(zoneSize.Y)); - } - - private Vector2 CalculateAdvance(DividerZone z, Vector2 windowPadding, Vector2 containerSize, Vector2 layoutMask) - { - Vector2 advance = containerSize * z.Size * layoutMask; - - DividerZone first = Zones.First(); - DividerZone last = Zones.Last(); - if (first != last && z == first) - { - advance += windowPadding * 0.5f * layoutMask; - } - - return new Vector2((float)Math.Round(advance.X), (float)Math.Round(advance.Y)); - } - - /// - /// Add a zone to the container. - /// - /// The ID of the zone. - /// The size of the zone. - /// Whether the zone is resizable. - /// The delegate to call when the zone is ticked. - public void Add(string id, float size, bool resizable, Action tickDelegate) => Zones.Add(new(id, size, resizable, tickDelegate)); - - /// - /// Add a zone to the container. - /// - /// The ID of the zone. - /// The size of the zone. - /// The delegate to call when the zone is ticked. - public void Add(string id, float size, Action tickDelegate) => Zones.Add(new(id, size, tickDelegate)); - - /// - /// Add a zone to the container. - /// - /// The ID of the zone. - /// The delegate to call when the zone is ticked. - public void Add(string id, Action tickDelegate) - { - float size = 1.0f / (Zones.Count + 1); - Zones.Add(new(id, size, tickDelegate)); - } - - /// - /// Add a zone to the container. - /// - /// The zone to add - public void Add(DividerZone zone) => Zones.Add(zone); - - /// - /// Remove a zone from the container. - /// - /// The ID of the zone to remove. - public void Remove(string id) - { - DividerZone? zone = Zones.FirstOrDefault(z => z.Id == id); - if (zone != null) - { - Zones.Remove(zone); - } - } - - /// - /// Remome all zones from the container. - /// - public void Clear() => Zones.Clear(); - - /// - /// Set the size of a zone by its ID. - /// - /// The ID of the zone to set the size of. - /// The size to set. - public void SetSize(string id, float size) - { - DividerZone? zone = Zones.FirstOrDefault(z => z.Id == id); - if (zone != null) - { - zone.Size = size; - } - } - - /// - /// Set the size of a zone by its index. - /// - /// The index of the zone to set the size of. - /// The size to set. - public void SetSize(int index, float size) - { - if (index >= 0 && index < Zones.Count) - { - Zones[index].Size = size; - } - } - - /// - /// Set the sizes of all zones in this container from a collection of sizes. - /// - /// The collection of sizes to set. - /// - public void SetSizesFromList(ICollection sizes) - { - ArgumentNullException.ThrowIfNull(sizes, nameof(sizes)); - - if (sizes.Count != Zones.Count) - { - throw new ArgumentException("List of sizes must be the same length as the zones list"); - } - - foreach ((float s, int i) in sizes.WithIndex()) - { - Zones[i].Size = s; - } - } - - /// - /// Get a collection of the sizes of all zones in this container. - /// - /// A collection of the sizes of all zones in this container. - public Collection GetSizes() => Zones.Select(z => z.Size).ToCollection(); - } -} diff --git a/ImGui.Widgets/DividerZone.cs b/ImGui.Widgets/DividerZone.cs deleted file mode 100644 index 983ac14..0000000 --- a/ImGui.Widgets/DividerZone.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// A zone that can be resized by dragging a divider. - /// For use inside DividerContainer. - /// - public class DividerZone - { - /// - /// The unique identifier for this zone. - /// - public string Id { get; private set; } - /// - /// The size of this zone. - /// - public float Size { get; set; } - /// - /// Whether this zone can be resized. - /// - public bool Resizable { get; } = true; - private Action? TickDelegate { get; } - internal float InitialSize { get; init; } - - /// - /// Create a new divider zone. - /// - /// The unique identifier for this zone. - /// The size of this zone. - public DividerZone(string id, float size) - { - Id = id; - Size = size; - InitialSize = size; - Resizable = true; - } - - /// - /// Create a new resizable divider zone with a tick delegate. - /// - /// The unique identifier for this zone. - /// The size of this zone. - /// The delegate to call every frame. - public DividerZone(string id, float size, Action tickDelegate) - { - Id = id; - Size = size; - InitialSize = size; - TickDelegate = tickDelegate; - } - - /// - /// Create a new divider zone that is optionally resizable with a tick delegate. - /// - /// The unique identifier for this zone. - /// The size of this zone. - /// Whether this zone can be resized. - /// The delegate to call every frame. - public DividerZone(string id, float size, bool resizable, Action tickDelegate) - { - Id = id; - Size = size; - InitialSize = size; - Resizable = resizable; - TickDelegate = tickDelegate; - } - - /// - /// Create a new divider zone that is optionally resizable. - /// - /// The unique identifier for this zone. - /// The size of this zone. - /// Whether this zone can be resized. - public DividerZone(string id, float size, bool resizable) - { - Id = id; - Size = size; - InitialSize = size; - Resizable = resizable; - } - - /// - /// Calls the tick delegate if it is set. - /// - /// The delta time since the last tick. - internal void Tick(float dt) => TickDelegate?.Invoke(dt); - } -} diff --git a/ImGui.Widgets/Grid.cs b/ImGui.Widgets/Grid.cs deleted file mode 100644 index 9221a0c..0000000 --- a/ImGui.Widgets/Grid.cs +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Drawing; -using System.Numerics; - -using Hexa.NET.ImGui; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Gets or sets a value indicating whether to enable grid debug drawing. - /// - public static bool EnableGridDebugDraw { get; set; } - - /// - /// Specifies the order in which grid items are displayed. - /// - /// - /// displays items left to right before moving to the next row. - /// displays items top to bottom before moving to the next column. - /// - public enum GridOrder - { - /// - /// Items are displayed in order left to right before dropping to the next row. - /// Recommended for when displaying grids of icons. - /// Example: - /// [ [1] [2] [3] ] - /// [ [4] [5] [6] ] - /// OR - /// [ [1] [2] [3] [4] [5] ] - /// [ [6] ] - /// - RowMajor, - /// - /// Items are displayed top to bottom before moving to the next column. - /// Recommended when displaying tables of data. - /// Example: - /// [ [1] [4] ] - /// [ [2] [5] ] - /// [ [3] [6] ] - /// OR - /// [ [1] [5] ] - /// [ [2] [6] ] - /// [ [3] ] - /// [ [4] ] - /// - ColumnMajor, - } - - /// - /// Options for changing how the grid is laid out. - /// - public class GridOptions - { - /// - /// Size of the grid. Setting any axis to 0 will use the available space. - /// - public Vector2 GridSize { get; set; } = new(0, 0); - - /// - /// Size the content region to cover all the items. - /// - public bool FitToContents { get; init; } - } - - /// - /// Delegate to measure the size of a grid cell. - /// - /// The type of the item. - /// The item to measure. - /// The size of the item. - public delegate Vector2 MeasureGridCell(T item); - - /// - /// Delegate to draw a grid cell. - /// - /// The type of the item. - /// The item to draw. - /// The calculated size of the grid cell. - /// The calculated size of the item. - public delegate void DrawGridCell(T item, Vector2 cellSize, Vector2 itemSize); - - /// - /// Renders a grid with the specified items and delegates. - /// - /// The type of the items. - /// Id for the grid. - /// The items to be displayed in the grid. - /// The delegate to measure the size of each item. - /// The delegate to draw each item. - public static void RowMajorGrid(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(measureDelegate); - ArgumentNullException.ThrowIfNull(drawDelegate); - - GridImpl.ShowRowMajor(id, items, measureDelegate, drawDelegate, new()); - } - - /// - /// Renders a grid with the specified items and delegates. - /// - /// The type of the items. - /// Id for the grid. - /// The items to be displayed in the grid. - /// The delegate to measure the size of each item. - /// The delegate to draw each item. - /// Additional options to modify the grid behaviour - public static void RowMajorGrid(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate, GridOptions gridOptions) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(measureDelegate); - ArgumentNullException.ThrowIfNull(drawDelegate); - ArgumentNullException.ThrowIfNull(gridOptions); - - GridImpl.ShowRowMajor(id, items, measureDelegate, drawDelegate, gridOptions); - } - - /// - /// Renders a grid with the specified items and delegates. - /// - /// The type of the items. - /// Id for the grid. - /// The items to be displayed in the grid. - /// The delegate to measure the size of each item. - /// The delegate to draw each item. - public static void ColumnMajorGrid(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(measureDelegate); - ArgumentNullException.ThrowIfNull(drawDelegate); - - GridImpl.ShowColumnMajor(id, items, measureDelegate, drawDelegate, new()); - } - - /// - /// Renders a grid with the specified items and delegates. - /// - /// The type of the items. - /// Id for the grid. - /// The items to be displayed in the grid. - /// The delegate to measure the size of each item. - /// The delegate to draw each item. - /// Additional options to modify the grid behaviour - public static void ColumnMajorGrid(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate, GridOptions gridOptions) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(measureDelegate); - ArgumentNullException.ThrowIfNull(drawDelegate); - ArgumentNullException.ThrowIfNull(gridOptions); - - GridImpl.ShowColumnMajor(id, items, measureDelegate, drawDelegate, gridOptions); - } - - /// - /// Contains the implementation details for rendering grids. - /// - internal static class GridImpl - { - internal sealed class GridLayout() - { - internal Point Dimensions { private get; init; } - internal float[] ColumnWidths { get; init; } = []; - internal float[] RowHeights { get; init; } = []; - internal int ColumnCount => Dimensions.X; - internal int RowCount => Dimensions.Y; - } - - #region RowMajor - private static Point CalculateRowMajorCellLocation(int itemIndex, int columnCount) - { - int columnIndex = itemIndex % columnCount; - int rowIndex = itemIndex / columnCount; - return new(columnIndex, rowIndex); - } - - private static GridLayout CalculateRowMajorGridLayout(Vector2[] itemSizes, float containerWidth) - { - float widestElementHeight = itemSizes.Max(itemSize => itemSize.X); - GridLayout currentGridLayout = new() - { - Dimensions = new(1, itemSizes.Length), - ColumnWidths = [widestElementHeight], - RowHeights = [.. itemSizes.Select(itemSize => itemSize.Y)] - }; - - GridLayout previousGridLayout = currentGridLayout; - while (currentGridLayout.ColumnCount < itemSizes.Length) - { - int newColumnCount = currentGridLayout.ColumnCount + 1; - int newRowCount = (int)MathF.Ceiling(itemSizes.Length / (float)newColumnCount); - currentGridLayout = new() - { - Dimensions = new(newColumnCount, newRowCount), - ColumnWidths = new float[newColumnCount], - RowHeights = new float[newRowCount], - }; - - for (int i = 0; i < itemSizes.Length; i++) - { - Vector2 itemSize = itemSizes[i]; - Point cellLocation = CalculateRowMajorCellLocation(i, newColumnCount); - - float maxColumnWidth = currentGridLayout.ColumnWidths[cellLocation.X]; - maxColumnWidth = Math.Max(maxColumnWidth, itemSize.X); - currentGridLayout.ColumnWidths[cellLocation.X] = maxColumnWidth; - - float maxRowHeight = currentGridLayout.RowHeights[cellLocation.Y]; - maxRowHeight = Math.Max(maxRowHeight, itemSize.Y); - currentGridLayout.RowHeights[cellLocation.Y] = maxRowHeight; - } - - if (currentGridLayout.ColumnWidths.Sum() > containerWidth) - { - currentGridLayout = previousGridLayout; - break; - } - - previousGridLayout = currentGridLayout; - } - - return currentGridLayout; - } - - internal static void ShowRowMajor(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate, GridOptions gridOptions) - { - if (gridOptions.GridSize.X <= 0) - { - gridOptions.GridSize = new(ImGui.GetContentRegionAvail().X, gridOptions.GridSize.Y); - } - - if (gridOptions.GridSize.Y <= 0) - { - gridOptions.GridSize = new(gridOptions.GridSize.X, ImGui.GetContentRegionAvail().Y); - } - - Vector2 itemSpacing = ImGui.GetStyle().ItemSpacing; - T[] itemList = [.. items]; - int itemListCount = itemList.Length; - Vector2[] itemDimensions = [.. itemList.Select(i => measureDelegate(i))]; - Vector2[] itemDimensionsWithSpacing = [.. itemDimensions.Select(d => d + itemSpacing)]; - - if (itemList.Length == 0) - { - // No items to display - if (!gridOptions.FitToContents) - { - ImGui.Dummy(gridOptions.GridSize); - } - - return; - } - - GridLayout gridLayout = CalculateRowMajorGridLayout(itemDimensionsWithSpacing, gridOptions.GridSize.X); - - if (gridOptions.FitToContents) - { - float width = gridLayout.ColumnWidths.Sum(); - float height = gridLayout.RowHeights.Sum(); - gridOptions.GridSize = new(width, height); - } - - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - uint borderColor = ImGui.GetColorU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Border]); - if (ImGui.BeginChild($"rowMajorGrid_{id}", gridOptions.GridSize, ImGuiChildFlags.None)) - { - Vector2 gridTopLeft = ImGui.GetCursorScreenPos(); - if (EnableGridDebugDraw) - { - drawList.AddRect(gridTopLeft, gridTopLeft + gridOptions.GridSize, borderColor); - } - - Vector2 rowTopleft = gridTopLeft; - for (int rowIndex = 0; rowIndex < gridLayout.RowCount; rowIndex++) - { - bool isFirstRow = rowIndex == 0; - float previousRowHeight = isFirstRow ? 0f : gridLayout.RowHeights[rowIndex - 1]; - - float columnCursorX = rowTopleft.X; - float columnCursorY = rowTopleft.Y + previousRowHeight; - rowTopleft = new(columnCursorX, columnCursorY); - ImGui.SetCursorScreenPos(rowTopleft); - - Vector2 cellTopLeft = ImGui.GetCursorScreenPos(); - int itemBeginIndex = rowIndex * gridLayout.ColumnCount; - int itemEndIndex = Math.Min(itemBeginIndex + gridLayout.ColumnCount, itemListCount); - for (int itemIndex = itemBeginIndex; itemIndex < itemEndIndex; itemIndex++) - { - int columnIndex = itemIndex - itemBeginIndex; - bool isFirstColumn = itemIndex == itemBeginIndex; - float previousCellWidth = isFirstColumn ? 0f : gridLayout.ColumnWidths[columnIndex - 1]; - - float cellCursorX = cellTopLeft.X + previousCellWidth; - float cellCursorY = cellTopLeft.Y; - cellTopLeft = new(cellCursorX, cellCursorY); - ImGui.SetCursorScreenPos(cellTopLeft); - - float cellWidth = gridLayout.ColumnWidths[columnIndex]; - float cellHeight = gridLayout.RowHeights[rowIndex]; - Vector2 cellSize = new(cellWidth, cellHeight); - - if (EnableGridDebugDraw) - { - drawList.AddRect(cellTopLeft, cellTopLeft + cellSize, borderColor); - } - - drawDelegate(itemList[itemIndex], cellSize, itemDimensions[itemIndex]); - } - } - } - - ImGui.EndChild(); - } - #endregion - - #region ColumnMajor - private static Point CalculateColumnMajorCellLocation(int itemIndex, int rowCount) - { - int columnIndex = itemIndex / rowCount; - int rowIndex = itemIndex % rowCount; - return new(columnIndex, rowIndex); - } - - private static GridLayout CalculateColumnMajorGridLayout(Vector2[] itemSizes, float containerHeight) - { - float tallestElementHeight = itemSizes.Max(itemSize => itemSize.Y); - GridLayout currentGridLayout = new() - { - Dimensions = new(itemSizes.Length, 1), - ColumnWidths = [.. itemSizes.Select(itemSize => itemSize.X)], - RowHeights = [tallestElementHeight], - }; - - GridLayout previousGridLayout = currentGridLayout; - while (currentGridLayout.RowCount < itemSizes.Length) - { - int newRowCount = currentGridLayout.RowCount + 1; - int newColumnCount = (int)MathF.Ceiling(itemSizes.Length / (float)newRowCount); - currentGridLayout = new() - { - Dimensions = new(newColumnCount, newRowCount), - ColumnWidths = new float[newColumnCount], - RowHeights = new float[newRowCount], - }; - - for (int i = 0; i < itemSizes.Length; i++) - { - Vector2 itemSize = itemSizes[i]; - Point cellLocation = CalculateColumnMajorCellLocation(i, newRowCount); - - float maxColumnWidth = currentGridLayout.ColumnWidths[cellLocation.X]; - maxColumnWidth = Math.Max(maxColumnWidth, itemSize.X); - currentGridLayout.ColumnWidths[cellLocation.X] = maxColumnWidth; - - float maxRowHeight = currentGridLayout.RowHeights[cellLocation.Y]; - maxRowHeight = Math.Max(maxRowHeight, itemSize.Y); - currentGridLayout.RowHeights[cellLocation.Y] = maxRowHeight; - } - - if (currentGridLayout.RowHeights.Sum() > containerHeight) - { - currentGridLayout = previousGridLayout; - break; - } - - previousGridLayout = currentGridLayout; - } - - return currentGridLayout; - } - - internal static void ShowColumnMajor(string id, IEnumerable items, MeasureGridCell measureDelegate, DrawGridCell drawDelegate, GridOptions gridOptions) - { - if (gridOptions.GridSize.X <= 0) - { - gridOptions.GridSize = new(ImGui.GetContentRegionAvail().X, gridOptions.GridSize.Y); - } - - if (gridOptions.GridSize.Y <= 0) - { - gridOptions.GridSize = new(gridOptions.GridSize.X, ImGui.GetContentRegionAvail().Y); - } - - Vector2 itemSpacing = ImGui.GetStyle().ItemSpacing; - T[] itemList = [.. items]; - int itemListCount = itemList.Length; - Vector2[] itemDimensions = [.. itemList.Select(i => measureDelegate(i))]; - Vector2[] itemDimensionsWithSpacing = [.. itemDimensions.Select(d => d + itemSpacing)]; - - if (itemList.Length == 0) - { - // No items to display - if (!gridOptions.FitToContents) - { - ImGui.Dummy(gridOptions.GridSize); - } - - return; - } - - GridLayout gridLayout = CalculateColumnMajorGridLayout(itemDimensionsWithSpacing, gridOptions.GridSize.Y); - - if (gridOptions.FitToContents) - { - float width = gridLayout.ColumnWidths.Sum(); - float height = gridLayout.RowHeights.Sum(); - gridOptions.GridSize = new(width, height); - } - - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - uint borderColor = ImGui.GetColorU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Border]); - if (ImGui.BeginChild($"columnMajorGrid_{id}", gridOptions.GridSize, ImGuiChildFlags.None, ImGuiWindowFlags.HorizontalScrollbar)) - { - Vector2 gridTopLeft = ImGui.GetCursorScreenPos(); - if (EnableGridDebugDraw) - { - drawList.AddRect(gridTopLeft, gridTopLeft + gridOptions.GridSize, borderColor); - } - - Vector2 columnTopLeft = gridTopLeft; - for (int columnIndex = 0; columnIndex < gridLayout.ColumnCount; columnIndex++) - { - bool isFirstColumn = columnIndex == 0; - float previousColumnWidth = isFirstColumn ? 0f : gridLayout.ColumnWidths[columnIndex - 1]; - - float columnCursorX = columnTopLeft.X + previousColumnWidth; - float columnCursorY = columnTopLeft.Y; - columnTopLeft = new(columnCursorX, columnCursorY); - ImGui.SetCursorScreenPos(columnTopLeft); - - Vector2 cellTopLeft = ImGui.GetCursorScreenPos(); - int itemBeginIndex = columnIndex * gridLayout.RowCount; - int itemEndIndex = Math.Min(itemBeginIndex + gridLayout.RowCount, itemListCount); - for (int itemIndex = itemBeginIndex; itemIndex < itemEndIndex; itemIndex++) - { - int rowIndex = itemIndex - itemBeginIndex; - bool isFirstRow = itemIndex == itemBeginIndex; - float previousCellHeight = isFirstRow ? 0f : itemDimensionsWithSpacing[rowIndex - 1].Y; - - float cellCursorX = cellTopLeft.X; - float cellCursorY = cellTopLeft.Y + previousCellHeight; - cellTopLeft = new(cellCursorX, cellCursorY); - ImGui.SetCursorScreenPos(cellTopLeft); - - float cellWidth = gridLayout.ColumnWidths[columnIndex]; - float cellHeight = gridLayout.RowHeights[rowIndex]; - Vector2 cellSize = new(cellWidth, cellHeight); - - if (EnableGridDebugDraw) - { - drawList.AddRect(cellTopLeft, cellTopLeft + cellSize, borderColor); - } - - drawDelegate(itemList[itemIndex], cellSize, itemDimensions[itemIndex]); - } - } - } - - ImGui.EndChild(); - } - #endregion - } -} diff --git a/ImGui.Widgets/Icon.cs b/ImGui.Widgets/Icon.cs deleted file mode 100644 index 6921dc0..0000000 --- a/ImGui.Widgets/Icon.cs +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.Styler; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Gets or sets a value indicating whether to enable debug drawing for icons. - /// - public static bool EnableIconDebugDraw { get; set; } - - /// - /// Specifies the alignment of the icon. - /// - public enum IconAlignment - { - /// - /// Aligns the icon horizontally. - /// - Horizontal, - - /// - /// Aligns the icon vertically. - /// - Vertical, - } - - /// - /// Additional options to modify Icon behavior. - /// - public class IconOptions - { - /// - /// The color of the icon. - /// - public Vector4 Color { get; init; } = Styler.Color.Palette.Neutral.White.Value; - - /// - /// The tooltip to display. - /// - public string Tooltip { get; init; } = string.Empty; - - /// - /// Gets or sets the action to be performed on click. - /// - public Action? OnClick { get; init; } - - /// - /// Gets or sets the action to be performed on double click. - /// - public Action? OnDoubleClick { get; init; } - - /// - /// Gets or sets the action to be performed on right click. - /// - public Action? OnRightClick { get; init; } - - /// - /// Gets or sets the action to be performed on context menu. - /// - public Action? OnContextMenu { get; init; } - } - - /// - /// Renders an icon with the specified parameters. - /// - /// The label of the icon. - /// The texture ID of the icon. - /// The size of the image. - /// The alignment of the icon. - /// Was the icon bounds clicked - public static bool Icon(string label, uint textureId, float imageSize, IconAlignment iconAlignment) => - IconImpl.Show(label, textureId, new(imageSize, imageSize), iconAlignment, new()); - - /// - /// Renders an icon with the specified parameters. - /// - /// The label of the icon. - /// The texture ID of the icon. - /// The size of the image. - /// The alignment of the icon. - /// Was the icon bounds clicked - public static bool Icon(string label, uint textureId, Vector2 imageSize, IconAlignment iconAlignment) => - IconImpl.Show(label, textureId, imageSize, iconAlignment, new()); - - /// - /// Renders an icon with the specified parameters. - /// - /// The label of the icon. - /// The texture ID of the icon. - /// The size of the image. - /// The alignment of the icon. - /// Additional options - /// Was the icon bounds clicked - public static bool Icon(string label, uint textureId, float imageSize, IconAlignment iconAlignment, IconOptions options) => - IconImpl.Show(label, textureId, new(imageSize, imageSize), iconAlignment, options); - - /// - /// Renders an icon with the specified parameters. - /// - /// The label of the icon. - /// The texture ID of the icon. - /// The size of the image. - /// The alignment of the icon. - /// Additional options - /// Was the icon bounds clicked - public static bool Icon(string label, uint textureId, Vector2 imageSize, IconAlignment iconAlignment, IconOptions options) => - IconImpl.Show(label, textureId, imageSize, iconAlignment, options); - - /// - /// Calculates the size of the icon with the specified parameters. - /// - /// The label of the icon. - /// The size of the image. - /// The alignment of the icon. - /// The calculated size of the icon. - public static Vector2 CalcIconSize(string label, float imageSize, IconAlignment iconAlignment) => CalcIconSize(label, new Vector2(imageSize), iconAlignment); - - /// - /// Calculates the size of the icon with the specified parameters. - /// - /// The label of the icon. - /// The size of the image. - /// The calculated size of the icon. - public static Vector2 CalcIconSize(string label, Vector2 imageSize) => CalcIconSize(label, imageSize, IconAlignment.Horizontal); - - /// - /// Calculates the size of the icon with the specified parameters. - /// - /// The label of the icon. - /// The size of the image. - /// The alignment of the image and label with respect to each other. - /// The calculated size of the widget. - public static Vector2 CalcIconSize(string label, Vector2 imageSize, IconAlignment iconAlignment) - { - ImGuiStylePtr style = ImGui.GetStyle(); - Vector2 framePadding = style.FramePadding; - Vector2 itemSpacing = style.ItemSpacing; - Vector2 labelSize = ImGui.CalcTextSize(label); - if (iconAlignment == IconAlignment.Horizontal) - { - Vector2 boundingBoxSize = imageSize + new Vector2(labelSize.X + itemSpacing.X, 0); - boundingBoxSize.Y = Math.Max(boundingBoxSize.Y, labelSize.Y); - return boundingBoxSize + (framePadding * 2); - } - else if (iconAlignment == IconAlignment.Vertical) - { - Vector2 boundingBoxSize = imageSize + new Vector2(0, labelSize.Y + itemSpacing.Y); - boundingBoxSize.X = Math.Max(boundingBoxSize.X, labelSize.X); - return boundingBoxSize + (framePadding * 2); - } - - return imageSize; - } - - /// - /// Contains the implementation details for rendering icons. - /// - internal static class IconImpl - { - internal static bool Show(string label, uint textureId, Vector2 imageSize, IconAlignment iconAlignment, IconOptions options) - { - ArgumentNullException.ThrowIfNull(label); - ArgumentNullException.ThrowIfNull(options); - - bool wasClicked = false; - - ImGuiStylePtr style = ImGui.GetStyle(); - Vector2 framePadding = style.FramePadding; - Vector2 itemSpacing = style.ItemSpacing; - - ImGui.PushID(label); - - Vector2 cursorStartPos = ImGui.GetCursorScreenPos(); - Vector2 labelSize = ImGui.CalcTextSize(label);// TODO, maybe pass this to an internal overload of CalcIconSize to save recalculating - Vector2 boundingBoxSize = CalcIconSize(label, imageSize, iconAlignment); - - ImGui.SetCursorScreenPos(cursorStartPos + framePadding); - - switch (iconAlignment) - { - case IconAlignment.Horizontal: - HorizontalLayout(label, textureId, imageSize, labelSize, boundingBoxSize, itemSpacing, options.Color, cursorStartPos); - break; - case IconAlignment.Vertical: - VerticalLayout(label, textureId, imageSize, labelSize, boundingBoxSize, itemSpacing, options.Color, cursorStartPos); - break; - default: - throw new NotImplementedException(); - } - - ImGui.SetCursorScreenPos(cursorStartPos); - ImGui.Dummy(boundingBoxSize); - bool isHovered = ImGui.IsItemHovered(); - bool isMouseClicked = ImGui.IsMouseClicked(ImGuiMouseButton.Left); - bool isMouseDoubleClicked = ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left); - bool isRightMouseClicked = ImGui.IsMouseClicked(ImGuiMouseButton.Right); - bool isRightMouseReleased = ImGui.IsMouseReleased(ImGuiMouseButton.Right); - - if (!string.IsNullOrEmpty(options.Tooltip)) - { - ImGui.SetItemTooltip(options.Tooltip); - } - - if (isHovered || EnableIconDebugDraw) - { - uint borderColor = ImGui.GetColorU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Border]); - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - drawList.AddRect(cursorStartPos, cursorStartPos + boundingBoxSize, ImGui.GetColorU32(borderColor)); - } - - if (isHovered) - { - if (isMouseClicked) - { - options.OnClick?.Invoke(); - wasClicked = true; - } - - if (isMouseDoubleClicked) - { - options.OnDoubleClick?.Invoke(); - } - - if (isRightMouseClicked) - { - options.OnRightClick?.Invoke(); - } - - if (isRightMouseReleased && options.OnContextMenu is not null) - { - ImGui.OpenPopup($"{label}_Context"); - } - } - - if (ImGui.BeginPopup($"{label}_Context")) - { - options.OnContextMenu?.Invoke(); - ImGui.EndPopup(); - } - - ImGui.PopID(); - - return wasClicked; - } - - private static void VerticalLayout(string label, uint textureId, Vector2 imageSize, Vector2 labelSize, Vector2 boundingBoxSize, Vector2 itemSpacing, Vector4 color = default, Vector2 cursorStartPos = default) - { - Vector2 imageTopLeft = cursorStartPos + new Vector2((boundingBoxSize.X - imageSize.X) / 2, 0); - ImGui.SetCursorScreenPos(imageTopLeft); - unsafe - { - if (color != default) - { - // Use transparent background with color as tint to preserve alpha - ImGui.ImageWithBg(new ImTextureRef(texId: textureId), imageSize, Vector4.Zero, color); - } - else - { - ImGui.Image(new ImTextureRef(texId: textureId), imageSize); - } - } - - Vector2 labelTopLeft = cursorStartPos + new Vector2((boundingBoxSize.X - labelSize.X) / 2, imageSize.Y + itemSpacing.Y); - ImGui.SetCursorScreenPos(labelTopLeft); - ImGui.TextUnformatted(label); - } - - private static void HorizontalLayout(string label, uint textureId, Vector2 imageSize, Vector2 labelSize, Vector2 boundingBoxSize, Vector2 itemSpacing, Vector4 color = default, Vector2 cursorStartPos = default) - { - unsafe - { - if (color != default) - { - // Use transparent background with color as tint to preserve alpha - ImGui.ImageWithBg(new ImTextureRef(texId: textureId), imageSize, Vector4.Zero, color); - } - else - { - ImGui.Image(new ImTextureRef(texId: textureId), imageSize); - } - } - Vector2 leftAlign = new(labelSize.X, boundingBoxSize.Y); - ImGui.SetCursorScreenPos(cursorStartPos + new Vector2(imageSize.X + itemSpacing.X, 0)); - using (new Alignment.CenterWithin(labelSize, leftAlign)) - { - ImGui.TextUnformatted(label); - } - } - } -} diff --git a/ImGui.Widgets/ImGui.Widgets.csproj b/ImGui.Widgets/ImGui.Widgets.csproj deleted file mode 100644 index d017b0a..0000000 --- a/ImGui.Widgets/ImGui.Widgets.csproj +++ /dev/null @@ -1,14 +0,0 @@ -īģŋ - - - - - True - net8.0;net9.0 - - - - - - - diff --git a/ImGui.Widgets/Image.cs b/ImGui.Widgets/Image.cs deleted file mode 100644 index 18a811d..0000000 --- a/ImGui.Widgets/Image.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.Styler; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Displays an image with the specified texture ID and size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// True if the image is clicked; otherwise, false. - public static bool Image(uint textureId, Vector2 size) => ImageImpl.Show(textureId, size, Vector4.One); - - /// - /// Displays an image with the specified texture ID, size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - public static bool Image(uint textureId, Vector2 size, Vector4 color) => ImageImpl.Show(textureId, size, color); - - /// - /// Displays a centered image with the specified texture ID and size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// True if the image is clicked; otherwise, false. - public static bool ImageCentered(uint textureId, Vector2 size) => ImageImpl.Centered(textureId, size, Vector4.One); - - /// - /// Displays a centered image with the specified texture ID, size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - public static bool ImageCentered(uint textureId, Vector2 size, Vector4 color) => ImageImpl.Centered(textureId, size, color); - - /// - /// Displays a centered image within a container with the specified texture ID, size, and container size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The size of the container. - /// True if the image is clicked; otherwise, false. - public static bool ImageCenteredWithin(uint textureId, Vector2 size, Vector2 containerSize) => ImageImpl.CenteredWithin(textureId, size, containerSize, Vector4.One); - - /// - /// Displays a centered image within a container with the specified texture ID, size, container size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The size of the container. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - public static bool ImageCenteredWithin(uint textureId, Vector2 size, Vector2 containerSize, Vector4 color) => ImageImpl.CenteredWithin(textureId, size, containerSize, color); - - internal static class ImageImpl - { - /// - /// Displays an image with the specified texture ID and size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// True if the image is clicked; otherwise, false. - internal static bool Show(uint textureId, Vector2 size) => Show(textureId, size, Vector4.One); - - /// - /// Displays an image with the specified texture ID, size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - internal static bool Show(uint textureId, Vector2 size, Vector4 color = default) - { - unsafe - { - if (color != default) - { - // Use transparent background with color as tint to preserve alpha - ImGui.ImageWithBg(new ImTextureRef(texId: textureId), size, Vector4.Zero, color); - } - else - { - ImGui.Image(new ImTextureRef(texId: textureId), size); - } - } - return ImGui.IsItemClicked(); - } - - /// - /// Displays a centered image with the specified texture ID and size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// True if the image is clicked; otherwise, false. - internal static bool Centered(uint textureId, Vector2 size) => Centered(textureId, size, Vector4.One); - - /// - /// Displays a centered image with the specified texture ID, size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - internal static bool Centered(uint textureId, Vector2 size, Vector4 color) - { - bool clicked = false; - using (new Alignment.Center(size)) - { - clicked = Show(textureId, size, color); - } - - return clicked; - } - - /// - /// Displays a centered image within a container with the specified texture ID, size, and container size. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The size of the container. - /// True if the image is clicked; otherwise, false. - internal static bool CenteredWithin(uint textureId, Vector2 size, Vector2 containerSize) => CenteredWithin(textureId, size, containerSize, Vector4.One); - - /// - /// Displays a centered image within a container with the specified texture ID, size, container size, and color. - /// - /// The ID of the texture to display. - /// The size of the image. - /// The size of the container. - /// The color to apply to the image. - /// True if the image is clicked; otherwise, false. - internal static bool CenteredWithin(uint textureId, Vector2 imageSize, Vector2 containerSize, Vector4 color) - { - bool clicked = false; - using (new Alignment.CenterWithin(imageSize, containerSize)) - { - clicked = Show(textureId, imageSize, color); - } - - return clicked; - } - } -} diff --git a/ImGui.Widgets/Knob.cs b/ImGui.Widgets/Knob.cs deleted file mode 100644 index 3fb5199..0000000 --- a/ImGui.Widgets/Knob.cs +++ /dev/null @@ -1,726 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; -using System; -using System.Globalization; -using System.Numerics; - -using Hexa.NET.ImGui; - -/// -/// Options for customizing the appearance and behavior of the knob widget. -/// -[Flags] -public enum ImGuiKnobOptions -{ - /// - /// No options selected. - /// - None = 0, - /// - /// Hides the title of the knob. - /// - NoTitle = 1 << 0, - /// - /// Disables the input field for the knob. - /// - NoInput = 1 << 1, - /// - /// Shows a tooltip with the current value when hovering over the knob. - /// - ValueTooltip = 1 << 2, - /// - /// Allows horizontal dragging to change the knob value. - /// - DragHorizontal = 1 << 3, - /// - /// Displays the title below the knob. - /// - TitleBelow = 1 << 4, -}; - -/// -/// Variants for customizing the visual appearance of the knob widget. -/// -[Flags] -public enum ImGuiKnobVariant -{ - /// - /// Represents a knob variant with tick marks. - /// - Tick = 1 << 0, - /// - /// Represents a knob variant with a dot indicator. - /// - Dot = 1 << 1, - /// - /// Represents a knob variant with a wiper indicator. - /// - Wiper = 1 << 2, - /// - /// Represents a knob variant with only a wiper indicator. - /// - WiperOnly = 1 << 3, - /// - /// Represents a knob variant with a wiper and dot indicator. - /// - WiperDot = 1 << 4, - /// - /// Represents a knob variant with stepped values. - /// - Stepped = 1 << 5, - /// - /// Represents a knob variant with a space theme. - /// - Space = 1 << 6, -}; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Draws a knob widget with a floating-point value. - /// - /// The label for the knob. - /// The current value of the knob. - /// The minimum value of the knob. - /// The maximum value of the knob. - /// The speed at which the knob value changes. - /// The format string for displaying the value. - /// The visual variant of the knob. - /// The size of the knob. - /// The options for the knob. - /// The number of steps for the knob. - /// True if the value was changed, otherwise false. - public static bool Knob(string label, ref float value, float vMin, float vMax, float speed = 0, string? format = null, ImGuiKnobVariant variant = ImGuiKnobVariant.Tick, float size = 0, ImGuiKnobOptions flags = ImGuiKnobOptions.None, int steps = 10) => - KnobImpl.Draw(label, ref value, vMin, vMax, speed, format, variant, size, flags, steps); - - /// - /// Draws a knob widget with an integer value. - /// - /// The label for the knob. - /// The current value of the knob. - /// The minimum value of the knob. - /// The maximum value of the knob. - /// The speed at which the knob value changes. - /// The format string for displaying the value. - /// The visual variant of the knob. - /// The size of the knob. - /// The options for the knob. - /// The number of steps for the knob. - /// True if the value was changed, otherwise false. - public static bool Knob(string label, ref int value, int vMin, int vMax, float speed = 0, string? format = null, ImGuiKnobVariant variant = ImGuiKnobVariant.Tick, float size = 0, ImGuiKnobOptions flags = ImGuiKnobOptions.None, int steps = 10) => - KnobImpl.Draw(label, ref value, vMin, vMax, speed, format, variant, size, flags, steps); - /// - /// Knob widget for ImGui.NET - /// - internal static class KnobImpl - { - public sealed class KnobColors - { - public ImColor Base { get; set; } - public ImColor Hovered { get; set; } - public ImColor Active { get; set; } - - public KnobColors() { } - - public KnobColors(ImColor color) - { - Base = color; - Hovered = color; - Active = color; - } - } - - public static bool Draw(string label, ref float value, float vMin, float vMax, float speed = 0, string? format = null, ImGuiKnobVariant variant = ImGuiKnobVariant.Tick, float? size = null, ImGuiKnobOptions flags = ImGuiKnobOptions.None, int steps = 10) - { - format ??= "%.3f"; - return KnobInternal.BaseKnob(label, ImGuiDataType.Float, ref value, vMin, vMax, speed, format, variant, size, flags, steps); - } - - public static bool Draw(string label, ref int value, int vMin, int vMax, float speed = 0, string? format = null, ImGuiKnobVariant variant = ImGuiKnobVariant.Tick, float? size = null, ImGuiKnobOptions flags = ImGuiKnobOptions.None, int steps = 10) - { - format ??= "%if"; - return KnobInternal.BaseKnob(label, ImGuiDataType.S32, ref value, vMin, vMax, speed, format, variant, size, flags, steps); - } - - private static void DrawArc1(Vector2 center, float radius, float startAngle, float endAngle, float thickness, ImColor color, int numSegments) - { - Vector2 start = new( - center[0] + (MathF.Cos(startAngle) * radius), - center[1] + (MathF.Sin(startAngle) * radius)); - - Vector2 end = new( - center[0] + (MathF.Cos(endAngle) * radius), - center[1] + (MathF.Sin(endAngle) * radius)); - - // Calculate bezier arc points - float ax = start[0] - center[0]; - float ay = start[1] - center[1]; - float bx = end[0] - center[0]; - float by = end[1] - center[1]; - float q1 = (ax * ax) + (ay * ay); - float q2 = q1 + (ax * bx) + (ay * by); - float k2 = 4.0f / 3.0f * (MathF.Sqrt(2.0f * q1 * q2) - q2) / ((ax * by) - (ay * bx)); - Vector2 arc1 = new(center[0] + ax - (k2 * ay), center[1] + ay + (k2 * ax)); - Vector2 arc2 = new(center[0] + bx + (k2 * by), center[1] + by - (k2 * bx)); - - ImDrawListPtr drawlist = ImGui.GetWindowDrawList(); - - drawlist.AddBezierCubic(start, arc1, arc2, end, ImGui.GetColorU32(color.Value), thickness, numSegments); - } - - internal static void DrawArc(Vector2 center, float radius, float startAngle, float endAngle, float thickness, ImColor color, int numSegments, int bezierCount) - { - // Overlap and angle of ends of bezier curves needs work, only looks good when not transperant - float overlap = thickness * radius * 0.00001f * MathF.PI; - float delta = endAngle - startAngle; - float bez_step = 1.0f / bezierCount; - float mid_angle = startAngle + overlap; - - for (int i = 0; i < bezierCount - 1; i++) - { - float mid_angle2 = (delta * bez_step) + mid_angle; - DrawArc1(center, radius, mid_angle - overlap, mid_angle2 + overlap, thickness, color, numSegments); - mid_angle = mid_angle2; - } - - DrawArc1(center, radius, mid_angle - overlap, endAngle, thickness, color, numSegments); - } - - private sealed class KnobInternal where TDataType : unmanaged, INumber - { - public float Radius { get; set; } - public bool ValueChanged { get; set; } - public Vector2 Center { get; set; } - public bool IsActive { get; set; } - public bool IsHovered { get; set; } - public float AngleMin { get; set; } - public float AngleMax { get; set; } - public float T { get; set; } - public float Angle { get; set; } - public float AngleCos { get; set; } - public float AngleSin { get; set; } - - private static float AccumulatedDiff { get; set; } - private static bool AccumulatorDirty { get; set; } - - private static float InverseLerp(TDataType min, TDataType max, TDataType value) => float.CreateSaturating(value - min) / float.CreateSaturating(max - min); - - public KnobInternal(string label_, ImGuiDataType dataType, ref TDataType value, TDataType vMin, TDataType vMax, float speed, float radius_, string format, ImGuiKnobOptions flags) - { - Radius = radius_; - T = InverseLerp(vMin, vMax, value); - Vector2 screenPos = ImGui.GetCursorScreenPos(); - - // Handle dragging - ImGui.InvisibleButton(label_, new(Radius * 2.0f, Radius * 2.0f)); - - ValueChanged = DragBehavior(dataType, ref value, vMin, vMax, speed, format, flags); - - AngleMin = MathF.PI * 0.75f; - AngleMax = MathF.PI * 2.25f; - Center = new(screenPos[0] + Radius, screenPos[1] + Radius); - Angle = AngleMin + ((AngleMax - AngleMin) * T); - AngleCos = MathF.Cos(Angle); - AngleSin = MathF.Sin(Angle); - } - - private bool DragBehavior(ImGuiDataType dataType, ref TDataType v, TDataType vMin, TDataType vMax, float speed, string format, ImGuiKnobOptions flags) - { - float floatValue = float.CreateSaturating(v); - float floatMin = float.CreateSaturating(vMin); - float floatMax = float.CreateSaturating(vMax); - bool isClamped = vMin < vMax; - float range = floatMax - floatMin; - if (speed == 0.0f && isClamped && (range < float.MaxValue)) - { - speed = range * 0.01f; - } - - bool justActivated = ImGui.IsItemActivated(); - IsActive = ImGui.IsItemActive(); - IsHovered = ImGui.IsItemHovered(); - - bool isFloatingPoint = dataType is ImGuiDataType.Float or ImGuiDataType.Double; - int decimalPrecision = isFloatingPoint ? ParseFormatPrecision(format, 3) : 0; - speed = MathF.Max(speed, GetMinimumStepAtDecimalPrecision(decimalPrecision)); - - Vector2 mouseDelta = ImGui.GetIO().MouseDelta; - float diff = (flags.HasFlag(ImGuiKnobOptions.DragHorizontal) ? mouseDelta.X : -mouseDelta.Y) * speed; - - diff = IsActive ? diff : 0.0f; - - if (justActivated) - { - AccumulatedDiff = 0.0f; - AccumulatorDirty = false; - } - else if (diff != 0.0f) - { - AccumulatedDiff += diff; - AccumulatorDirty = true; - } - - if (!AccumulatorDirty) - { - return false; - } - - float newValue = floatValue + diff; - - // Round to user desired precision based on format string - if (isFloatingPoint) - { - newValue = MathF.Round(newValue, decimalPrecision); - } - - float appliedDiff = newValue - floatValue; - AccumulatedDiff -= appliedDiff; - AccumulatorDirty = false; - - if (newValue == -0.0f) - { - newValue = 0.0f; - } - - // Clamp values (+ handle overflow/wrap-around for integer types) - if (newValue != floatValue && isClamped) - { - if (newValue < floatMin || (newValue > floatValue && diff < 0.0f && !isFloatingPoint)) - { - newValue = floatMin; - } - - if (newValue > floatMax || (newValue < floatValue && diff > 0.0f && !isFloatingPoint)) - { - newValue = floatMax; - } - } - - if (newValue != floatValue) - { - v = TDataType.CreateSaturating(newValue); - return true; - } - - return false; - } - - private static int ParseFormatPrecision(string fmt, int defaultPrecision) - { - - ReadOnlySpan fmtSpan = ParseFormatFindStart(fmt); - if (fmtSpan[0] != '%') - { - return defaultPrecision; - } - - fmtSpan = fmtSpan[1..]; - while (fmtSpan[0] is >= '0' and <= '9') - { - fmtSpan = fmtSpan[1..]; - } - - int precision = int.MaxValue; - if (fmtSpan[0] == '.') - { - fmtSpan = fmtSpan[1..]; - int precisionLength = 0; - while (fmtSpan[precisionLength] is >= '0' and <= '9') - { - precisionLength++; - } - - precision = int.Parse(fmtSpan[..precisionLength], CultureInfo.CurrentCulture); - fmtSpan = fmtSpan[precisionLength..]; - - if (precision is < 0 or > 99) - { - precision = defaultPrecision; - } - } - - if (fmtSpan[0] is 'e' or 'E') // Maximum precision with scientific notation - { - precision = -1; - } - - if ((fmtSpan[0] == 'g' || fmtSpan[0] == 'G') && precision == int.MaxValue) - { - precision = -1; - } - - return (precision == int.MaxValue) ? defaultPrecision : precision; - } - - private static ReadOnlySpan ParseFormatFindStart(string fmt) - { - ReadOnlySpan fmtSpan = fmt.AsSpan(); - while (fmtSpan.Length > 2) - { - char c = fmtSpan[0]; - if (c == '%' && fmtSpan[1] != '%') - { - return fmtSpan; - } - else if (c == '%') - { - fmtSpan = fmtSpan[1..]; - } - - fmtSpan = fmtSpan[1..]; - } - - return fmtSpan; - } - - private static readonly List MinSteps = [1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f]; - private static float GetMinimumStepAtDecimalPrecision(int decimal_precision) - { - return decimal_precision < 0 - ? float.MinValue - : (decimal_precision < MinSteps.Count) ? MinSteps[decimal_precision] : MathF.Pow(10.0f, -decimal_precision); - } - - private void DrawDot(float size, float radius, float angle, KnobColors color, int segments) - { - float dotSize = size * Radius; - float dotRadius = radius * Radius; - - ImGui.GetWindowDrawList().AddCircleFilled( - new(Center[0] + (MathF.Cos(angle) * dotRadius), Center[1] + (MathF.Sin(angle) * dotRadius)), - dotSize, - ImGui.GetColorU32((IsActive ? color.Active : (IsHovered ? color.Hovered : color.Base)).Value), - segments); - } - - private void DrawTick(float start, float end, float width, float angle, KnobColors color) - { - float tickStart = start * Radius; - float tickEnd = end * Radius; - float angleCos = MathF.Cos(angle); - float angleSin = MathF.Sin(angle); - - ImGui.GetWindowDrawList().AddLine( - - new(Center[0] + (angleCos * tickEnd), Center[1] + (angleSin * tickEnd)), - new(Center[0] + (angleCos * tickStart), Center[1] + (angleSin * tickStart)), - ImGui.GetColorU32((IsActive ? color.Active : (IsHovered ? color.Hovered : color.Base)).Value), - width * Radius); - } - - private void DrawCircle(float size, KnobColors color, int segments) - { - float circleRadius = size * Radius; - - ImGui.GetWindowDrawList().AddCircleFilled( - Center, - circleRadius, - ImGui.GetColorU32((IsActive ? color.Active : (IsHovered ? color.Hovered : color.Base)).Value), - segments); - } - - private void DrawArc(float radius, float size, float startAngle, float endAngle, KnobColors color, int segments, int bezierCount) - { - float trackRadius = radius * Radius; - float trackSize = (size * Radius * 0.5f) + 0.0001f; - - KnobImpl.DrawArc( - Center, - trackRadius, - startAngle, - endAngle, - trackSize, - IsActive ? color.Active : (IsHovered ? color.Hovered : color.Base), - segments, - bezierCount); - } - - private static List WrapStringToWidth(string text, float width) - { - List lines = []; - string line; - ReadOnlySpan textSpan = text.AsSpan(); - float textWidth = ImGui.CalcTextSize(text).X; - - if (textWidth <= width) - { - lines.Add(text); - return lines; - } - - while (textSpan.Length > 0) - { - while (textSpan.StartsWith(" ")) - { - textSpan = textSpan[1..]; - } - - while (textSpan.EndsWith(" ")) - { - textSpan = textSpan[..(textSpan.Length - 1)]; - } - - ReadOnlySpan lineSpan = textSpan; - - float lineSize = ImGui.CalcTextSize(lineSpan.ToString()).X; - - while (lineSize > width) - { - int lastSpace = lineSpan.LastIndexOf(' '); - if (lastSpace == -1) - { - break; - } - - lineSpan = lineSpan[..lastSpace]; - while (lineSpan.StartsWith(" ")) - { - lineSpan = lineSpan[1..]; - } - - while (lineSpan.EndsWith(" ")) - { - lineSpan = lineSpan[..(lineSpan.Length - 1)]; - } - - lineSize = ImGui.CalcTextSize(lineSpan.ToString()).X; - } - - line = lineSpan.ToString(); - lines.Add(line); - textSpan = textSpan[line.Length..]; - } - - return lines; - } - - public static KnobInternal KnobWithDrag(string label, ImGuiDataType dataType, ref TDataType value, TDataType vMin, TDataType vMax, float speed, string format, float? size, ImGuiKnobOptions flags) - { - speed = speed == 0 ? float.CreateSaturating(vMax - vMin) / 250.0f : speed; - ImGui.PushID(label); - float lineBasedHeight = ImGui.GetTextLineHeight() * 4.0f; - float width = size ?? lineBasedHeight; - if (width == 0) - { - width = lineBasedHeight; - } - - List titleLines = WrapStringToWidth(label, width); - - float maxTitleLineWidth = 0.0f; - - if (!flags.HasFlag(ImGuiKnobOptions.NoTitle)) - { - maxTitleLineWidth = titleLines.Max(line => ImGui.CalcTextSize(line).X); - } - - maxTitleLineWidth = Math.Max(maxTitleLineWidth, width); - float knobPadding = (maxTitleLineWidth - width) * 0.5f; - - ImGui.PushItemWidth(width); - - ImGui.BeginGroup(); - - // There's an issue with `SameLine` and Groups, see https://github.com/ocornut/imgui/issues/4190. - // This is probably not the best solution, but seems to work for now - //ImGui.GetCurrentWindow().DC.CurrLineTextBaseOffset = 0; - - if (!flags.HasFlag(ImGuiKnobOptions.TitleBelow)) - { - DrawTitle(flags, maxTitleLineWidth, titleLines); - } - - // Draw knob - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + knobPadding); - KnobInternal k = new(label, dataType, ref value, vMin, vMax, speed, width * 0.5f, format, flags); - - // Draw tooltip - if (flags.HasFlag(ImGuiKnobOptions.ValueTooltip) && (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) || ImGui.IsItemActive())) - { - ImGui.BeginTooltip(); - ImGui.Text(string.Format(CultureInfo.CurrentCulture, format, value)); - ImGui.EndTooltip(); - } - - // Draw input - if (!flags.HasFlag(ImGuiKnobOptions.NoInput)) - { - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + knobPadding); - unsafe - { - fixed (TDataType* pValue = &value) - { - TDataType* pMin = &vMin; - TDataType* pMax = &vMax; - k.ValueChanged = ImGui.DragScalar("###knob_drag", dataType, pValue, speed, pMin, pMax, format); - } - } - } - - if (flags.HasFlag(ImGuiKnobOptions.TitleBelow)) - { - DrawTitle(flags, maxTitleLineWidth, titleLines); - } - - ImGui.EndGroup(); - ImGui.PopItemWidth(); - ImGui.PopID(); - - return k; - - static void DrawTitle(ImGuiKnobOptions flags, float width, List titleLines) - { - if (!flags.HasFlag(ImGuiKnobOptions.NoTitle)) - { - foreach (string line in titleLines) - { - Vector2 lineWidth = ImGui.CalcTextSize(line, false, width); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ((width - lineWidth[0]) * 0.5f)); - ImGui.TextUnformatted(line); - } - } - } - } - - public static bool BaseKnob(string label, ImGuiDataType dataType, ref TDataType value, TDataType vMin, TDataType vMax, float speed, string format, ImGuiKnobVariant variant, float? size, ImGuiKnobOptions flags, int steps = 10) - { - - KnobInternal knob = KnobWithDrag(label, dataType, ref value, vMin, vMax, speed, format, size, flags); - - switch (variant) - { - case ImGuiKnobVariant.Tick: - { - knob.DrawCircle(0.85f, GetSecondaryColorSet(), 32); - knob.DrawTick(0.5f, 0.85f, 0.08f, knob.Angle, GetPrimaryColorSet()); - break; - } - case ImGuiKnobVariant.Dot: - { - knob.DrawCircle(0.85f, GetSecondaryColorSet(), 32); - knob.DrawDot(0.12f, 0.6f, knob.Angle, GetPrimaryColorSet(), 12); - break; - } - - case ImGuiKnobVariant.Wiper: - { - knob.DrawCircle(0.7f, GetSecondaryColorSet(), 32); - knob.DrawArc(0.8f, 0.41f, knob.AngleMin, knob.AngleMax, GetTrackColorSet(), 16, 2); - - if (knob.T > 0.01f) - { - knob.DrawArc(0.8f, 0.43f, knob.AngleMin, knob.Angle, GetPrimaryColorSet(), 16, 2); - } - - break; - } - case ImGuiKnobVariant.WiperOnly: - { - knob.DrawArc(0.8f, 0.41f, knob.AngleMin, knob.AngleMax, GetTrackColorSet(), 32, 2); - - if (knob.T > 0.01) - { - knob.DrawArc(0.8f, 0.43f, knob.AngleMin, knob.Angle, GetPrimaryColorSet(), 16, 2); - } - - break; - } - case ImGuiKnobVariant.WiperDot: - { - knob.DrawCircle(0.6f, GetSecondaryColorSet(), 32); - knob.DrawArc(0.85f, 0.41f, knob.AngleMin, knob.AngleMax, GetTrackColorSet(), 16, 2); - knob.DrawDot(0.1f, 0.85f, knob.Angle, GetPrimaryColorSet(), 12); - break; - } - case ImGuiKnobVariant.Stepped: - { - for (float n = 0.0f; n < steps; n++) - { - float a = n / (steps - 1); - float angle = knob.AngleMin + ((knob.AngleMax - knob.AngleMin) * a); - knob.DrawTick(0.7f, 0.9f, 0.04f, angle, GetPrimaryColorSet()); - } - - knob.DrawCircle(0.6f, GetSecondaryColorSet(), 32); - knob.DrawDot(0.12f, 0.4f, knob.Angle, GetPrimaryColorSet(), 12); - break; - } - case ImGuiKnobVariant.Space: - { - knob.DrawCircle(0.3f - (knob.T * 0.1f), GetSecondaryColorSet(), 16); - - if (knob.T > 0.01f) - { - knob.DrawArc(0.4f, 0.15f, knob.AngleMin - 1.0f, knob.Angle - 1.0f, GetPrimaryColorSet(), 16, 2); - knob.DrawArc(0.6f, 0.15f, knob.AngleMin + 1.0f, knob.Angle + 1.0f, GetPrimaryColorSet(), 16, 2); - knob.DrawArc(0.8f, 0.15f, knob.AngleMin + 3.0f, knob.Angle + 3.0f, GetPrimaryColorSet(), 16, 2); - } - - break; - } - - default: - break; - } - - return knob.ValueChanged; - } - } - - private static KnobColors GetPrimaryColorSet() - { - Span colors = ImGui.GetStyle().Colors; - - return new() - { - Active = new ImColor() { Value = colors[(int)ImGuiCol.ButtonActive] }, - Hovered = new ImColor() { Value = colors[(int)ImGuiCol.ButtonHovered] }, - Base = new ImColor() { Value = colors[(int)ImGuiCol.ButtonHovered] }, - }; - } - - private static KnobColors GetSecondaryColorSet() - { - Span colors = ImGui.GetStyle().Colors; - Vector4 activeColor = colors[(int)ImGuiCol.ButtonActive]; - Vector4 hoveredColor = colors[(int)ImGuiCol.ButtonHovered]; - - Vector4 active = new( - activeColor.X * 0.5f, - activeColor.Y * 0.5f, - activeColor.Z * 0.5f, - activeColor.W); - - Vector4 hovered = new( - hoveredColor.X * 0.5f, - hoveredColor.Y * 0.5f, - hoveredColor.Z * 0.5f, - hoveredColor.W); - - return new() - { - Active = new ImColor() { Value = active }, - Hovered = new ImColor() { Value = hovered }, - Base = new ImColor() { Value = hovered }, - }; - } - - private static KnobColors GetTrackColorSet() - { - Span colors = ImGui.GetStyle().Colors; - Vector4 color = colors[(int)ImGuiCol.FrameBg]; - return new() - { - Active = new ImColor() { Value = color }, - Hovered = new ImColor() { Value = color }, - Base = new ImColor() { Value = color }, - }; - } - } -} diff --git a/ImGui.Widgets/README.md b/ImGui.Widgets/README.md deleted file mode 100644 index 0728be3..0000000 --- a/ImGui.Widgets/README.md +++ /dev/null @@ -1,265 +0,0 @@ -# ktsu.ImGuiWidgets - -ImGuiWidgets is a library of custom widgets using ImGui.NET. This library provides a variety of widgets and utilities to enhance your ImGui-based applications. - -## Features - -- **Knobs**: Ported to .NET from [ImGui-works/ImGui-knobs-dial-gauge-meter](https://github.com/imgui-works/imgui-knobs-dial-gauge-meter) -- **Resizable Layout Dividers**: Draggable layout dividers for resizable layouts -- **TabPanel**: Tabbed interface with closable, reorderable tabs and dirty indicator support -- **Icons**: Customizable icons with various alignment options and event delegates -- **Grid**: Flexible grid layout for displaying items -- **Color Indicator**: An indicator that displays a color when enabled -- **Image**: An image widget with alignment options -- **Text**: A text widget with alignment options -- **Tree**: A tree widget for displaying hierarchical data -- **Scoped Id**: A utility class for creating scoped IDs -- **SearchBox**: A powerful search box with support for various filter types (Glob, Regex, Fuzzy) and matching options - -## Installation - -To install ImGuiWidgets, you can add the library to your .NET project using the following command: - -```bash -dotnet add package ktsu.ImGuiWidgets -``` - -## Usage - -To use ImGuiWidgets, you need to include the `ktsu.ImGuiWidgets` namespace in your code: - -```csharp -using ktsu.ImGuiWidgets; -``` - -Then, you can start using the widgets provided by ImGuiWidgets in your ImGui-based applications. - -## Examples - -Here are some examples of using ImGuiWidgets: - -### Knobs - -Knobs are useful for creating dial-like controls: - -```csharp -float value = 0.5f; -float minValue = 0.0f; - -ImGuiWidgets.Knob("Knob", ref value, minValue); -``` - -### SearchBox - -The SearchBox widget provides a powerful search interface with multiple filter type options: - -```csharp -// Static fields to maintain filter state between renders -private static string searchTerm = string.Empty; -private static TextFilterType filterType = TextFilterType.Glob; -private static TextFilterMatchOptions matchOptions = TextFilterMatchOptions.ByWholeString; - -// List of items to search -var items = new List { "Apple", "Banana", "Cherry", "Date", "Elderberry" }; - -// Basic search box with right-click context menu for filter options -ImGuiWidgets.SearchBox("##BasicSearch", ref searchTerm, ref filterType, ref matchOptions); - -// Display results -if (!string.IsNullOrEmpty(searchTerm)) -{ - ImGui.TextUnformatted($"Search results for: {searchTerm}"); -} - -// Search box that returns filtered results directly -var filteredResults = ImGuiWidgets.SearchBox( - "##FilteredSearch", - ref searchTerm, - items, // Collection to filter - item => item, // Selector function to extract string from each item - ref filterType, - ref matchOptions).ToList(); - -// Ranked search box for fuzzy matching and ranked results -var rankedResults = ImGuiWidgets.SearchBoxRanked( - "##RankedSearch", - ref searchTerm, - items, - item => item).ToList(); -``` - -### TabPanel - -TabPanel creates a tabbed interface with support for closable tabs, reordering, and dirty state indication: - -```csharp -// Create a tab panel with closable and reorderable tabs -var tabPanel = new ImGuiWidgets.TabPanel("MyTabPanel", true, true); - -// Add tabs with explicit IDs (recommended for stability when tabs are reordered) -string tab1Id = tabPanel.AddTab("tab1", "First Tab", RenderTab1Content); -string tab2Id = tabPanel.AddTab("tab2", "Second Tab", RenderTab2Content); -string tab3Id = tabPanel.AddTab("tab3", "Third Tab", RenderTab3Content); - -// Draw the tab panel in your render loop -tabPanel.Draw(); - -// Methods to render tab content -void RenderTab1Content() -{ - ImGui.Text("Tab 1 Content"); - - // Mark tab as dirty when content changes - if (ImGui.Button("Edit")) - { - tabPanel.MarkTabDirty(tab1Id); - } - - // Mark tab as clean when content is saved - if (ImGui.Button("Save")) - { - tabPanel.MarkTabClean(tab1Id); - } -} - -void RenderTab2Content() -{ - ImGui.Text("Tab 2 Content"); -} - -void RenderTab3Content() -{ - ImGui.Text("Tab 3 Content"); -} -``` - -### Icons - -Icons can be used to display images with various alignment options and event delegates: - -```csharp -float iconWidthEms = 7.5f; -float iconWidthPx = ImGuiApp.EmsToPx(iconWidthEms); - -uint textureId = ImGuiApp.GetOrLoadTexture("icon.png"); - -ImGuiWidgets.Icon("Click Me", textureId, iconWidthPx, Color.White.Value, ImGuiWidgets.IconAlignment.Vertical, new ImGuiWidgets.IconDelegates() -{ - OnClick = () => MessageOK.Open("Click", "You clicked") -}); - -ImGui.SameLine(); -ImGuiWidgets.Icon("Double Click Me", textureId, iconWidthPx, Color.White.Value, ImGuiWidgets.IconAlignment.Vertical, new ImGuiWidgets.IconDelegates() -{ - OnDoubleClick = () => MessageOK.Open("Double Click", "You clicked twice") -}); - -ImGui.SameLine(); -ImGuiWidgets.Icon("Right Click Me", textureId, iconWidthPx, Color.White.Value, ImGuiWidgets.IconAlignment.Vertical, new ImGuiWidgets.IconDelegates() -{ - OnContextMenu = () => - { - ImGui.MenuItem("Context Menu Item 1"); - ImGui.MenuItem("Context Menu Item 2"); - ImGui.MenuItem("Context Menu Item 3"); - }, -}); -``` - -### Grid - -The grid layout allows you to display items in a flexible grid: - -```csharp -float iconSizeEms = 7.5f; -float iconSizePx = ImGuiApp.EmsToPx(iconSizeEms); - -uint textureId = ImGuiApp.GetOrLoadTexture("icon.png"); - -ImGuiWidgets.Grid(items, i => ImGuiWidgets.CalcIconSize(i, iconSizePx), (item, cellSize, itemSize) => -{ - ImGuiWidgets.Icon(item, textureId, iconSizePx, Color.White.Value); -}); -``` - -### Color Indicator - -The color indicator widget displays a color when enabled: - -```csharp -bool enabled = true; -Color color = Color.Red; - -ImGuiWidgets.ColorIndicator("Color Indicator", enabled, color); -``` - -### Image - -The image widget allows you to display images with alignment options: - -```csharp -uint textureId = ImGuiApp.GetOrLoadTexture("image.png"); - -ImGuiWidgets.Image(textureId, new Vector2(100, 100)); -``` - -### Text - -The text widget allows you to display text with alignment options: - -```csharp -ImGuiWidgets.Text("Hello, ImGuiWidgets!"); -ImGuiWidgets.TextCentered("Hello, ImGuiWidgets!"); -ImGuiWidgets.TextCenteredWithin("Hello, ImGuiWidgets!", new Vector2(100, 100)); -``` - -### Tree - -The tree widget allows you to display hierarchical data: - -```csharp -using (var tree = new ImGuiWidgets.Tree()) -{ - for (int i = 0; i < 5; i++) - { - using (tree.Child) - { - ImGui.Button($"Hello, Child {i}!"); - using (var subtree = new ImGuiWidgets.Tree()) - { - using (subtree.Child) - { - ImGui.Button($"Hello, Grandchild!"); - } - } - } - } -} -``` - -### Scoped Id - -The scoped ID utility class helps in creating scoped IDs for ImGui elements and ensuring they get popped appropriatley: - -```csharp -using (new ImGuiWidgets.ScopedId()) -{ - ImGui.Button("Hello, Scoped ID!"); -} -``` - -## Contributing - -Contributions are welcome! For feature requests, bug reports, or questions, please open an issue on the GitHub repository. If you would like to contribute code, please open a pull request with your changes. - -## Acknowledgements - -ImGuiWidgets is inspired by the following projects: - -- [ocornut/ImGui](https://github.com/ocornut/imgui) -- [ImGui.NET](https://github.com/ImGuiNET/ImGui.NET) -- [ImGui-works/ImGui-knobs-dial-gauge-meter](https://github.com/imgui-works/imgui-knobs-dial-gauge-meter) - -## License - -ImGuiWidgets is licensed under the MIT License. See [LICENSE](LICENSE) for more information. diff --git a/ImGui.Widgets/ScopedDisable.cs b/ImGui.Widgets/ScopedDisable.cs deleted file mode 100644 index e4e6914..0000000 --- a/ImGui.Widgets/ScopedDisable.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using Hexa.NET.ImGui; - -using ScopedAction; - -/// -/// Represents a scoped disable action which will set Dear ImGui elements as functionally and visually disabled until -/// the class is disposed. -/// -public class ScopedDisable : ScopedAction -{ - /// - /// Note as per the Dear ImGui documentation: "Those can be nested, but it cannot - /// be used to enable an already disabled section (a single BeginDisabled(true) - /// in the stack is enough to keep everything disabled)" - /// - /// Should the elements within the scope be disabled - public ScopedDisable(bool enabled) - { - ImGui.BeginDisabled(enabled); - OnClose = ImGui.EndDisabled; - } -} diff --git a/ImGui.Widgets/ScopedId.cs b/ImGui.Widgets/ScopedId.cs deleted file mode 100644 index d8d9688..0000000 --- a/ImGui.Widgets/ScopedId.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using Hexa.NET.ImGui; -using ktsu.ScopedAction; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Represents a scoped ID for Dear ImGui. This class ensures that the ID is pushed when the object is created - /// and popped when the object is disposed. - /// - public class ScopedId : ScopedAction - { - /// - /// Initializes a new instance of the class with a string ID. - /// - /// The string ID to push to ImGui. - public ScopedId(string id) - { - ImGui.PushID(id); - OnClose = ImGui.PopID; - } - - /// - /// Initializes a new instance of the class with an integer ID. - /// - /// The integer ID to push to ImGui. - public ScopedId(int id) - { - ImGui.PushID(id); - OnClose = ImGui.PopID; - } - } -} diff --git a/ImGui.Widgets/SearchBox.cs b/ImGui.Widgets/SearchBox.cs deleted file mode 100644 index 1bc0f4e..0000000 --- a/ImGui.Widgets/SearchBox.cs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System; -using System.Collections.Generic; -using System.Linq; - -using Hexa.NET.ImGui; - -using ktsu.TextFilter; - -public static partial class ImGuiWidgets -{ - /// - /// A search box that filters items using ktsu.TextFilter. - /// - /// Label for display and id. - /// Current filter text. - /// How to match the filter text (default: WholeString). - /// Type of filter to use (default: Glob). - /// True if the filter text changed, otherwise false. - public static bool SearchBox( - string label, - ref string filterText, - ref TextFilterType filterType, - ref TextFilterMatchOptions matchOptions - ) - { - string hint = TextFilter.GetHint(filterType) + "\nRight-click for options"; - - bool changed = ImGui.InputTextWithHint(label, hint, ref filterText, 256); - bool isHovered = ImGui.IsItemHovered(); - bool isRightMouseClicked = ImGui.IsMouseClicked(ImGuiMouseButton.Right); - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - { - ImGui.SetTooltip(hint); - } - - if (isHovered && isRightMouseClicked) - { - ImGui.OpenPopup(label + "##context"); - } - - if (ImGui.BeginPopup(label + "##context")) - { - bool isGlob = filterType == TextFilterType.Glob; - bool isRegex = filterType == TextFilterType.Regex; - bool isFuzzy = filterType == TextFilterType.Fuzzy; - - bool isWholeString = matchOptions == TextFilterMatchOptions.ByWholeString; - bool isAllWord = matchOptions == TextFilterMatchOptions.ByWordAll; - bool isAnyWord = matchOptions == TextFilterMatchOptions.ByWordAny; - - if (ImGui.MenuItem("Glob", "", ref isGlob)) - { - filterType = TextFilterType.Glob; - } - - if (ImGui.MenuItem("Regex", "", ref isRegex)) - { - filterType = TextFilterType.Regex; - } - - if (ImGui.MenuItem("Fuzzy", "", ref isFuzzy)) - { - filterType = TextFilterType.Fuzzy; - } - - ImGui.Separator(); - - if (ImGui.MenuItem("Whole String", "", ref isWholeString)) - { - matchOptions = TextFilterMatchOptions.ByWholeString; - } - - if (ImGui.MenuItem("All Words", "", ref isAllWord)) - { - matchOptions = TextFilterMatchOptions.ByWordAll; - } - - if (ImGui.MenuItem("Any Word", "", ref isAnyWord)) - { - matchOptions = TextFilterMatchOptions.ByWordAny; - } - - ImGui.EndPopup(); - } - - return changed; - } - - /// - /// A search box that filters items using ktsu.TextFilter and returns the filtered results. - /// - /// Type of items to filter. - /// Label for display and id. - /// Current filter text. - /// Collection of items to filter. - /// Function to extract the string to match against from each item. - /// How to match the filter text (default: WholeString). - /// Type of filter to use (default: Glob). - /// Filtered collection of items. - public static IEnumerable SearchBox( - string label, - ref string filterText, - IEnumerable items, - Func selector, - ref TextFilterType filterType, - ref TextFilterMatchOptions matchOptions - ) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(selector); - - SearchBox(label, ref filterText, ref filterType, ref matchOptions); - - if (string.IsNullOrWhiteSpace(filterText)) - { - return []; - } - - Dictionary keyedItems = items.ToDictionary(selector, item => item); - - return TextFilter.Filter(keyedItems.Keys, filterText, filterType, matchOptions) - .Select(x => keyedItems[x]); - } - - /// - /// A search box that ranks items using a fuzzy filter. - /// - /// Type of items to rank. - /// Label for display and id. - /// Current filter text. - /// Collection of items to rank. - /// Function to extract the string to match against from each item. - /// Ranked collection of items. - public static IEnumerable SearchBoxRanked( - string label, - ref string filterText, - IEnumerable items, - Func selector - ) - { - ArgumentNullException.ThrowIfNull(items); - ArgumentNullException.ThrowIfNull(selector); - - TextFilterType filterType = TextFilterType.Fuzzy; - TextFilterMatchOptions matchOptions = TextFilterMatchOptions.ByWholeString; - SearchBox(label, ref filterText, ref filterType, ref matchOptions); - - if (string.IsNullOrWhiteSpace(filterText)) - { - return items; - } - - Dictionary keyedItems = items.ToDictionary(selector, item => item); - return TextFilter.Rank(keyedItems.Keys, filterText) - .Select(x => keyedItems[x]); - } -} diff --git a/ImGui.Widgets/TabPanel.cs b/ImGui.Widgets/TabPanel.cs deleted file mode 100644 index bbaff4e..0000000 --- a/ImGui.Widgets/TabPanel.cs +++ /dev/null @@ -1,548 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System; -using System.Collections.ObjectModel; -using Hexa.NET.ImGui; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// A panel with tabs that can be used to organize content. - /// - public class TabPanel - { - /// - /// The unique identifier for this tab panel. - /// - public string Id { get; private set; } - - /// - /// The collection of tabs within this panel. - /// - public Collection Tabs { get; private set; } = []; - - /// - /// The index of the currently active tab. - /// - public int ActiveTabIndex { get; set; } - - /// - /// The ID of the currently active tab. - /// - public string? ActiveTabId => ActiveTabIndex >= 0 && ActiveTabIndex < Tabs.Count - ? Tabs[ActiveTabIndex].Id - : null; - - /// - /// Whether the tab bar is closable (allows closing tabs with an X button). - /// - public bool Closable { get; set; } - - /// - /// Whether the tab bar should be reorderable. - /// - public bool Reorderable { get; set; } - - /// - /// Gets the currently active tab or null if there are no tabs. - /// - public Tab? ActiveTab => Tabs.Count > 0 && ActiveTabIndex >= 0 && ActiveTabIndex < Tabs.Count - ? Tabs[ActiveTabIndex] - : null; - - private Action? TabChangedDelegate { get; } - private Action? TabChangedByIdDelegate { get; } - - /// - /// Create a new tab panel. - /// - /// The unique identifier for this tab panel. - public TabPanel(string id) => Id = id; - - /// - /// Create a new tab panel with options. - /// - /// The unique identifier for this tab panel. - /// Whether tabs can be closed. - /// Whether tabs can be reordered. - public TabPanel(string id, bool closable, bool reorderable) - { - Id = id; - Closable = closable; - Reorderable = reorderable; - } - - /// - /// Create a new tab panel with a callback for tab changes. - /// - /// The unique identifier for this tab panel. - /// The delegate to call when the active tab changes, passing the tab index. - public TabPanel(string id, Action tabChangedDelegate) - { - Id = id; - TabChangedDelegate = tabChangedDelegate; - } - - /// - /// Create a new tab panel with a callback for tab changes. - /// - /// The unique identifier for this tab panel. - /// The delegate to call when the active tab changes, passing the tab ID. - public TabPanel(string id, Action tabChangedDelegate) - { - Id = id; - TabChangedByIdDelegate = tabChangedDelegate; - } - - /// - /// Create a new tab panel with options and a callback for tab changes. - /// - /// The unique identifier for this tab panel. - /// Whether tabs can be closed. - /// Whether tabs can be reordered. - /// The delegate to call when the active tab changes, passing the tab index. - public TabPanel(string id, bool closable, bool reorderable, Action tabChangedDelegate) - { - Id = id; - Closable = closable; - Reorderable = reorderable; - TabChangedDelegate = tabChangedDelegate; - } - - /// - /// Create a new tab panel with options and a callback for tab changes. - /// - /// The unique identifier for this tab panel. - /// Whether tabs can be closed. - /// Whether tabs can be reordered. - /// The delegate to call when the active tab changes, passing the tab ID. - public TabPanel(string id, bool closable, bool reorderable, Action tabChangedDelegate) - { - Id = id; - Closable = closable; - Reorderable = reorderable; - TabChangedByIdDelegate = tabChangedDelegate; - } - - /// - /// Find a tab by its ID. - /// - /// The ID of the tab to find. - /// The tab if found, null otherwise. - public Tab? GetTabById(string tabId) - { - foreach (Tab tab in Tabs) - { - if (tab.Id == tabId) - { - return tab; - } - } - - return null; - } - - /// - /// Get the index of a tab by its ID. - /// - /// The ID of the tab to find. - /// The index of the tab if found, -1 otherwise. - public int GetTabIndex(string tabId) - { - for (int i = 0; i < Tabs.Count; i++) - { - if (Tabs[i].Id == tabId) - { - return i; - } - } - - return -1; - } - - /// - /// Add a new tab to the panel. - /// - /// The label of the tab. - /// The content drawing delegate for the tab. - /// The ID of the newly added tab. - public string AddTab(string label, Action content) - { - Tab tab = new(label, content); - Tabs.Add(tab); - return tab.Id; - } - - /// - /// Add a new tab to the panel with a specific ID. - /// - /// The unique ID for the tab. - /// The label of the tab. - /// The content drawing delegate for the tab. - /// The ID of the newly added tab. - public string AddTab(string id, string label, Action content) - { - Tab tab = new(id, label, content); - Tabs.Add(tab); - return tab.Id; - } - - /// - /// Add a new tab to the panel with specified dirty state. - /// - /// The label of the tab. - /// The content drawing delegate for the tab. - /// Initial dirty state of the tab. - /// The ID of the newly added tab. - public string AddTab(string label, Action content, bool isDirty) - { - Tab tab = new(label, content, isDirty); - Tabs.Add(tab); - return tab.Id; - } - - /// - /// Add a new tab to the panel with a specific ID and dirty state. - /// - /// The unique ID for the tab. - /// The label of the tab. - /// The content drawing delegate for the tab. - /// Initial dirty state of the tab. - /// The ID of the newly added tab. - public string AddTab(string id, string label, Action content, bool isDirty) - { - Tab tab = new(id, label, content, isDirty); - Tabs.Add(tab); - return tab.Id; - } - - /// - /// Mark a tab as dirty (having unsaved changes). - /// - /// The index of the tab to mark dirty. - /// True if successful, false if the index is out of range. - public bool MarkTabDirty(int index) - { - if (index >= 0 && index < Tabs.Count) - { - Tabs[index].MarkDirty(); - return true; - } - - return false; - } - - /// - /// Mark a tab as dirty (having unsaved changes). - /// - /// The ID of the tab to mark dirty. - /// True if successful, false if the tab was not found. - public bool MarkTabDirty(string tabId) - { - Tab? tab = GetTabById(tabId); - if (tab != null) - { - tab.MarkDirty(); - return true; - } - - return false; - } - - /// - /// Mark a tab as clean (having no unsaved changes). - /// - /// The index of the tab to mark clean. - /// True if successful, false if the index is out of range. - public bool MarkTabClean(int index) - { - if (index >= 0 && index < Tabs.Count) - { - Tabs[index].MarkClean(); - return true; - } - - return false; - } - - /// - /// Mark a tab as clean (having no unsaved changes). - /// - /// The ID of the tab to mark clean. - /// True if successful, false if the tab was not found. - public bool MarkTabClean(string tabId) - { - Tab? tab = GetTabById(tabId); - if (tab != null) - { - tab.MarkClean(); - return true; - } - - return false; - } - - /// - /// Mark the currently active tab as dirty. - /// - /// True if there is an active tab and it was marked dirty, false otherwise. - public bool MarkActiveTabDirty() - { - if (ActiveTab != null) - { - ActiveTab.MarkDirty(); - return true; - } - - return false; - } - - /// - /// Mark the currently active tab as clean. - /// - /// True if there is an active tab and it was marked clean, false otherwise. - public bool MarkActiveTabClean() - { - if (ActiveTab != null) - { - ActiveTab.MarkClean(); - return true; - } - - return false; - } - - /// - /// Check if the tab at the specified index is dirty. - /// - /// The index of the tab to check. - /// True if the tab is dirty, false otherwise or if the index is out of range. - public bool IsTabDirty(int index) => index >= 0 && index < Tabs.Count && Tabs[index].IsDirty; - - /// - /// Check if the tab with the specified ID is dirty. - /// - /// The ID of the tab to check. - /// True if the tab is dirty, false otherwise or if the tab was not found. - public bool IsTabDirty(string tabId) - { - Tab? tab = GetTabById(tabId); - return tab?.IsDirty ?? false; - } - - /// - /// Check if the active tab is dirty. - /// - /// True if the active tab is dirty, false otherwise or if there is no active tab. - public bool IsActiveTabDirty() => ActiveTab?.IsDirty ?? false; - - /// - /// Remove a tab from the panel by index. - /// - /// The index of the tab to remove. - /// True if the tab was removed, false otherwise. - public bool RemoveTab(int index) - { - if (index >= 0 && index < Tabs.Count) - { - Tabs.RemoveAt(index); - if (ActiveTabIndex >= Tabs.Count) - { - ActiveTabIndex = Math.Max(0, Tabs.Count - 1); - TabChangedDelegate?.Invoke(ActiveTabIndex); - TabChangedByIdDelegate?.Invoke(ActiveTabId ?? string.Empty); - } - - return true; - } - - return false; - } - - /// - /// Remove a tab from the panel by ID. - /// - /// The ID of the tab to remove. - /// True if the tab was removed, false otherwise. - public bool RemoveTab(string tabId) - { - int index = GetTabIndex(tabId); - return index >= 0 && RemoveTab(index); - } - - /// - /// Draw the tab panel. - /// - public void Draw() - { - if (Tabs.Count == 0) - { - return; - } - - ImGuiTabBarFlags flags = ImGuiTabBarFlags.None; - if (Reorderable) - { - flags |= ImGuiTabBarFlags.Reorderable; - } - - if (ImGui.BeginTabBar(Id, flags)) - { - for (int i = 0; i < Tabs.Count; i++) - { - Tab tab = Tabs[i]; - ImGuiTabItemFlags tabFlags = ImGuiTabItemFlags.None; - - // Use the UnsavedDocument flag for dirty indicator - if (tab.IsDirty) - { - tabFlags |= ImGuiTabItemFlags.UnsavedDocument; - } - - bool tabOpen = true; - - if (ImGui.BeginTabItem($"{tab.Label}##{tab.Id}", ref tabOpen, tabFlags)) - { - if (ActiveTabIndex != i) - { - ActiveTabIndex = i; - TabChangedDelegate?.Invoke(i); - TabChangedByIdDelegate?.Invoke(tab.Id); - } - - tab.Content?.Invoke(); - ImGui.EndTabItem(); - } - - if (Closable && !tabOpen) - { - RemoveTab(i); - i--; // Adjust index since we removed an item - } - } - - ImGui.EndTabBar(); - } - } - } - - /// - /// Represents a single tab within a TabPanel. - /// - public class Tab - { - private static int nextId = 1; - - /// - /// Gets a unique identifier for this tab. - /// - public string Id { get; } - - /// - /// The label displayed on the tab. - /// - public string Label { get; set; } - - /// - /// The action to invoke to draw the tab content. - /// - public Action? Content { get; set; } - - /// - /// Gets or sets whether this tab's content has unsaved changes. - /// - public bool IsDirty { get; set; } - - /// - /// Create a new tab with an auto-generated ID. - /// - /// The label of the tab. - public Tab(string label) - { - Id = $"tab_{nextId++}"; - Label = label; - } - - /// - /// Create a new tab with a specific ID. - /// - /// The unique ID for the tab. - /// The label of the tab. - public Tab(string id, string label) - { - Id = id; - Label = label; - } - - /// - /// Create a new tab with content and an auto-generated ID. - /// - /// The label of the tab. - /// The content drawing delegate for the tab. - public Tab(string label, Action content) - { - Id = $"tab_{nextId++}"; - Label = label; - Content = content; - } - - /// - /// Create a new tab with a specific ID and content. - /// - /// The unique ID for the tab. - /// The label of the tab. - /// The content drawing delegate for the tab. - public Tab(string id, string label, Action content) - { - Id = id; - Label = label; - Content = content; - } - - /// - /// Create a new tab with content, dirty state, and an auto-generated ID. - /// - /// The label of the tab. - /// The content drawing delegate for the tab. - /// Initial dirty state of the tab. - public Tab(string label, Action content, bool isDirty) - { - Id = $"tab_{nextId++}"; - Label = label; - Content = content; - IsDirty = isDirty; - } - - /// - /// Create a new tab with a specific ID, content, and dirty state. - /// - /// The unique ID for the tab. - /// The label of the tab. - /// The content drawing delegate for the tab. - /// Initial dirty state of the tab. - public Tab(string id, string label, Action content, bool isDirty) - { - Id = id; - Label = label; - Content = content; - IsDirty = isDirty; - } - - /// - /// Marks the tab as dirty (having unsaved changes). - /// - public void MarkDirty() => IsDirty = true; - - /// - /// Marks the tab as clean (no unsaved changes). - /// - public void MarkClean() => IsDirty = false; - } -} diff --git a/ImGui.Widgets/Text.cs b/ImGui.Widgets/Text.cs deleted file mode 100644 index 7996b73..0000000 --- a/ImGui.Widgets/Text.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.Styler; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Displays the specified text. - /// - /// The text to display. - public static void Text(string text) => TextImpl.Show(text); - - /// - /// Displays the specified text centered within the available space. - /// - /// The text to display. - public static void TextCentered(string text) => TextImpl.Centered(text); - - /// - /// Displays the specified text centered horizontally within the given bounds. - /// - /// The text to display. - /// The size of the container within which the text will be centered. - public static void TextCenteredWithin(string text, Vector2 containerSize) => TextImpl.CenteredWithin(text, containerSize); - - internal static class TextImpl - { - /// - /// Displays the specified text. - /// - /// The text to display. - public static void Show(string text) => ImGui.TextUnformatted(text); - - /// - /// Displays the specified text centered within the available space. - /// - /// The text to display. - public static void Centered(string text) - { - Vector2 textSize = ImGui.CalcTextSize(text); - using (new Alignment.Center(textSize)) - { - ImGui.TextUnformatted(text); - } - } - - /// - /// Displays the specified text centered horizontally within the given bounds. - /// - /// The text to display. - /// The size of the container within which the text will be centered. - public static void CenteredWithin(string text, Vector2 containerSize) => CenteredWithin(text, containerSize, false); - - /// - /// Displays the specified text centered horizontally within the given bounds, with an option to clip the text. - /// - /// The text to display. - /// The size of the container within which the text will be centered. - /// If true, the text will be clipped to fit within the container size. - public static void CenteredWithin(string text, Vector2 containerSize, bool clip) - { - if (clip) - { - text = Clip(text, containerSize); - } - - Vector2 textSize = ImGui.CalcTextSize(text); - using (new Alignment.CenterWithin(textSize, containerSize)) - { - ImGui.TextUnformatted(text); - } - } - - /// - /// Clips the specified text to fit within the given container size, adding an ellipsis if necessary. - /// - /// The text to clip. - /// The size of the container within which the text must fit. - /// The clipped text with an ellipsis if it exceeds the container size. - public static string Clip(string text, Vector2 containerSize) - { - float textWidth = ImGui.CalcTextSize(text).X; - if (textWidth <= containerSize.X) - { - return text; - } - - string ellipsis = "..."; - float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X; - - while (textWidth + ellipsisWidth > containerSize.X && text.Length > 0) - { - text = text[..^1]; - textWidth = ImGui.CalcTextSize(text).X; - } - - return text + ellipsis; - } - } -} diff --git a/ImGui.Widgets/Tree.cs b/ImGui.Widgets/Tree.cs deleted file mode 100644 index 790a6a5..0000000 --- a/ImGui.Widgets/Tree.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Widgets; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.Styler; -using ktsu.ScopedAction; - -/// -/// Provides custom ImGui widgets. -/// -public static partial class ImGuiWidgets -{ - /// - /// Represents a tree structure widget in ImGui with custom drawing logic. - /// - public class Tree : ScopedAction - { - private Vector2 CursorStart { get; init; } - private Vector2 CursorEnd { get; set; } - private float IndentWidth { get; init; } - private float HalfIndentWidth => IndentWidth / 2f; - private float FrameHeight { get; init; } - private float HalfFrameHeight => FrameHeight / 2f; - private float ItemSpacingY { get; init; } - private float Left { get; init; } - private float Top { get; init; } - private const float LineThickness = 2f; - private const float HalfLineThickness = LineThickness / 2f; - - /// - /// Initializes a new instance of the class. - /// Sets up the initial cursor position, indent width, item spacing, frame height, and drawing logic for the tree structure. - /// - public Tree() : base() - { - ImGui.Indent(); - CursorStart = ImGui.GetCursorScreenPos(); - IndentWidth = ImGui.GetStyle().IndentSpacing; - ItemSpacingY = ImGui.GetStyle().ItemSpacing.Y; - FrameHeight = ImGui.GetFrameHeight(); - Left = CursorStart.X - HalfIndentWidth; - Top = CursorStart.Y - ItemSpacingY - HalfLineThickness; - - OnClose = () => - { - ImGui.SameLine(); - float bottom = CursorEnd.Y + HalfFrameHeight + HalfLineThickness; - Vector2 a = new(Left, Top); - Vector2 b = new(Left, bottom); - ImGui.GetWindowDrawList().AddLine(a, b, ImGui.GetColorU32(Color.Palette.Neutral.Gray.Value), LineThickness); - ImGui.NewLine(); - ImGui.Unindent(); - }; - } - - /// - /// Gets a new instance of the class, representing a child node in the tree structure. - /// - public TreeChild Child => new(this); - - /// - /// Represents a child node in the tree structure. - /// - /// The parent tree node. - public class TreeChild(Tree parent) : ScopedAction( - onOpen: () => - { - Vector2 cursor = ImGui.GetCursorScreenPos(); - parent.CursorEnd = cursor; - float right = cursor.X; - float y = cursor.Y + parent.HalfFrameHeight; - - Vector2 a = new(parent.Left, y); - Vector2 b = new(right, y); - - ImGui.GetWindowDrawList().AddLine(a, b, ImGui.GetColorU32(Color.Palette.Neutral.Gray.Value), LineThickness); - }, - onClose: null) - { - } - } -} diff --git a/ImGui.sln b/ImGui.sln deleted file mode 100644 index f588717..0000000 --- a/ImGui.sln +++ /dev/null @@ -1,148 +0,0 @@ -īģŋ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.App", "ImGui.App\ImGui.App.csproj", "{EEC56BFB-F502-8810-5DC7-9E17B3D17435}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.Popups", "ImGui.Popups\ImGui.Popups.csproj", "{C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.Styler", "ImGui.Styler\ImGui.Styler.csproj", "{8B4C0E01-82D0-E523-C654-8D33E2554884}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.Widgets", "ImGui.Widgets\ImGui.Widgets.csproj", "{0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" - ProjectSection(SolutionItems) = preProject - examples\ImGuiPopupsDemo\ImGuiPopupsDemo.csproj = examples\ImGuiPopupsDemo\ImGuiPopupsDemo.csproj - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiAppDemo", "examples\ImGuiAppDemo\ImGuiAppDemo.csproj", "{D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiStylerDemo", "examples\ImGuiStylerDemo\ImGuiStylerDemo.csproj", "{74E901E1-ED27-2FA9-D58D-48A875D82102}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiWidgetsDemo", "examples\ImGuiWidgetsDemo\ImGuiWidgetsDemo.csproj", "{723D183E-B609-CD2B-CF5B-B6F81E9368B3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{5F0E6F1C-44C1-42B7-ADF4-770ABB471B40}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.App.Tests", "tests\ImGui.App.Tests\ImGui.App.Tests.csproj", "{D1B3E399-F4E4-D537-46FE-2585AF4E9262}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|x64.Build.0 = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|x86.ActiveCfg = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Debug|x86.Build.0 = Debug|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|Any CPU.Build.0 = Release|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|x64.ActiveCfg = Release|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|x64.Build.0 = Release|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|x86.ActiveCfg = Release|Any CPU - {EEC56BFB-F502-8810-5DC7-9E17B3D17435}.Release|x86.Build.0 = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|x64.ActiveCfg = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|x64.Build.0 = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|x86.ActiveCfg = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Debug|x86.Build.0 = Debug|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|Any CPU.Build.0 = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|x64.ActiveCfg = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|x64.Build.0 = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|x86.ActiveCfg = Release|Any CPU - {C7889AF0-481D-EF98-D6F1-8DD9C0A44AB7}.Release|x86.Build.0 = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|x64.ActiveCfg = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|x64.Build.0 = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|x86.ActiveCfg = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Debug|x86.Build.0 = Debug|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|Any CPU.Build.0 = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|x64.ActiveCfg = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|x64.Build.0 = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|x86.ActiveCfg = Release|Any CPU - {8B4C0E01-82D0-E523-C654-8D33E2554884}.Release|x86.Build.0 = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|x64.ActiveCfg = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|x64.Build.0 = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|x86.ActiveCfg = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Debug|x86.Build.0 = Debug|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|Any CPU.Build.0 = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|x64.ActiveCfg = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|x64.Build.0 = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|x86.ActiveCfg = Release|Any CPU - {0BA81D84-EB0C-FDB5-8748-90F03AD17AC0}.Release|x86.Build.0 = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|x64.ActiveCfg = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|x64.Build.0 = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|x86.ActiveCfg = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Debug|x86.Build.0 = Debug|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|Any CPU.Build.0 = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|x64.ActiveCfg = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|x64.Build.0 = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|x86.ActiveCfg = Release|Any CPU - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961}.Release|x86.Build.0 = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|Any CPU.Build.0 = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|x64.ActiveCfg = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|x64.Build.0 = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|x86.ActiveCfg = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Debug|x86.Build.0 = Debug|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|Any CPU.ActiveCfg = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|Any CPU.Build.0 = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|x64.ActiveCfg = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|x64.Build.0 = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|x86.ActiveCfg = Release|Any CPU - {74E901E1-ED27-2FA9-D58D-48A875D82102}.Release|x86.Build.0 = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|x64.ActiveCfg = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|x64.Build.0 = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|x86.ActiveCfg = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Debug|x86.Build.0 = Debug|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|Any CPU.Build.0 = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|x64.ActiveCfg = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|x64.Build.0 = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|x86.ActiveCfg = Release|Any CPU - {723D183E-B609-CD2B-CF5B-B6F81E9368B3}.Release|x86.Build.0 = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|x64.ActiveCfg = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|x64.Build.0 = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|x86.ActiveCfg = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Debug|x86.Build.0 = Debug|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|Any CPU.Build.0 = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|x64.ActiveCfg = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|x64.Build.0 = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|x86.ActiveCfg = Release|Any CPU - {D1B3E399-F4E4-D537-46FE-2585AF4E9262}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D50ADE7F-2B62-EDB5-2F96-8E6E8D19B961} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {74E901E1-ED27-2FA9-D58D-48A875D82102} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {723D183E-B609-CD2B-CF5B-B6F81E9368B3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {D1B3E399-F4E4-D537-46FE-2585AF4E9262} = {5F0E6F1C-44C1-42B7-ADF4-770ABB471B40} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {14515AC1-A74C-4CBB-98F2-9DA660E967EF} - EndGlobalSection -EndGlobal diff --git a/tests/ImGui.App.Tests/AdvancedCoverageTests.cs b/ImGuiApp.Test/AdvancedCoverageTests.cs similarity index 98% rename from tests/ImGui.App.Tests/AdvancedCoverageTests.cs rename to ImGuiApp.Test/AdvancedCoverageTests.cs index 3037cfc..d0f28f6 100644 --- a/tests/ImGui.App.Tests/AdvancedCoverageTests.cs +++ b/ImGuiApp.Test/AdvancedCoverageTests.cs @@ -2,13 +2,13 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System; using Hexa.NET.ImGui; -using ktsu.ImGui.App.ImGuiController; +using ktsu.ImGuiApp.ImGuiController; +using ktsu.Semantics; using Microsoft.VisualStudio.TestTools.UnitTesting; -using ktsu.Semantics.Paths; /// /// Advanced tests targeting specific low-coverage areas including OpenGL functionality, shader management, and texture operations. diff --git a/tests/ImGui.App.Tests/ErrorHandlingAndEdgeCaseTests.cs b/ImGuiApp.Test/ErrorHandlingAndEdgeCaseTests.cs similarity index 99% rename from tests/ImGui.App.Tests/ErrorHandlingAndEdgeCaseTests.cs rename to ImGuiApp.Test/ErrorHandlingAndEdgeCaseTests.cs index dccecd9..017f9ee 100644 --- a/tests/ImGui.App.Tests/ErrorHandlingAndEdgeCaseTests.cs +++ b/ImGuiApp.Test/ErrorHandlingAndEdgeCaseTests.cs @@ -2,12 +2,11 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System; using System.Collections.Generic; -using ktsu.ImGui.App.ImGuiController; -using ktsu.ImGui.App; +using ktsu.ImGuiApp.ImGuiController; using Microsoft.VisualStudio.TestTools.UnitTesting; /// diff --git a/tests/ImGui.App.Tests/FontAndUITests.cs b/ImGuiApp.Test/FontAndUITests.cs similarity index 99% rename from tests/ImGui.App.Tests/FontAndUITests.cs rename to ImGuiApp.Test/FontAndUITests.cs index 8645c7b..6a36789 100644 --- a/tests/ImGui.App.Tests/FontAndUITests.cs +++ b/ImGuiApp.Test/FontAndUITests.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/tests/ImGui.App.Tests/ForceDpiAwareTests.cs b/ImGuiApp.Test/ForceDpiAwareTests.cs similarity index 95% rename from tests/ImGui.App.Tests/ForceDpiAwareTests.cs rename to ImGuiApp.Test/ForceDpiAwareTests.cs index 3e35c79..8676515 100644 --- a/tests/ImGui.App.Tests/ForceDpiAwareTests.cs +++ b/ImGuiApp.Test/ForceDpiAwareTests.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/tests/ImGui.App.Tests/GlobalScaleTests.cs b/ImGuiApp.Test/GlobalScaleTests.cs similarity index 88% rename from tests/ImGui.App.Tests/GlobalScaleTests.cs rename to ImGuiApp.Test/GlobalScaleTests.cs index 88e841e..b37b8cf 100644 --- a/tests/ImGui.App.Tests/GlobalScaleTests.cs +++ b/ImGuiApp.Test/GlobalScaleTests.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -101,21 +101,7 @@ public void SetGlobalScale_CallsCallback_WhenConfigured() } [TestMethod] - public void SetGlobalScale_NoCallback_DoesNotThrow() - { - // Arrange - ImGuiApp.Config = new ImGuiAppConfig - { - OnGlobalScaleChanged = null! - }; - - // Act & Assert - Should not throw - ImGuiApp.SetGlobalScale(1.5f); - Assert.AreEqual(1.5f, ImGuiApp.GlobalScale, 0.001f); - } - - [TestMethod] - public void SetGlobalScale_MultipleValues_CallbackInvokedEachTime() + public void SetGlobalScale_MultipleInvocations_CallsCallbackEachTime() { // Arrange int callbackCount = 0; diff --git a/ImGuiApp.Test/ImGuiApp.Test.csproj b/ImGuiApp.Test/ImGuiApp.Test.csproj new file mode 100644 index 0000000..88a9665 --- /dev/null +++ b/ImGuiApp.Test/ImGuiApp.Test.csproj @@ -0,0 +1,33 @@ + + + + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/tests/ImGui.App.Tests/ImGuiAppCoreTests.cs b/ImGuiApp.Test/ImGuiAppCoreTests.cs similarity index 98% rename from tests/ImGui.App.Tests/ImGuiAppCoreTests.cs rename to ImGuiApp.Test/ImGuiAppCoreTests.cs index bc021e4..f91f860 100644 --- a/tests/ImGui.App.Tests/ImGuiAppCoreTests.cs +++ b/ImGuiApp.Test/ImGuiAppCoreTests.cs @@ -2,11 +2,11 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; using System.Collections.Concurrent; +using ktsu.Extensions; +using ktsu.Semantics; using Microsoft.VisualStudio.TestTools.UnitTesting; /// diff --git a/tests/ImGui.App.Tests/ImGuiAppDataStructureTests.cs b/ImGuiApp.Test/ImGuiAppDataStructureTests.cs similarity index 98% rename from tests/ImGui.App.Tests/ImGuiAppDataStructureTests.cs rename to ImGuiApp.Test/ImGuiAppDataStructureTests.cs index 386965f..39cb960 100644 --- a/tests/ImGui.App.Tests/ImGuiAppDataStructureTests.cs +++ b/ImGuiApp.Test/ImGuiAppDataStructureTests.cs @@ -2,13 +2,13 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System.Numerics; +using ktsu.Extensions; +using ktsu.Semantics; using Microsoft.VisualStudio.TestTools.UnitTesting; using Silk.NET.Windowing; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; /// /// Tests for ImGuiApp data structures including texture info, window state, configuration, and performance settings. diff --git a/tests/ImGui.App.Tests/ImGuiAppTests.cs b/ImGuiApp.Test/ImGuiAppTests.cs similarity index 58% rename from tests/ImGui.App.Tests/ImGuiAppTests.cs rename to ImGuiApp.Test/ImGuiAppTests.cs index 857e89b..a8de52b 100644 --- a/tests/ImGui.App.Tests/ImGuiAppTests.cs +++ b/ImGuiApp.Test/ImGuiAppTests.cs @@ -4,16 +4,16 @@ [assembly: DoNotParallelize] -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System.Numerics; +using ktsu.Extensions; +using ktsu.Semantics; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Silk.NET.Core.Contexts; using Silk.NET.Maths; using Silk.NET.Windowing; -using ktsu.Semantics.Strings; -using ktsu.Semantics.Paths; [TestClass] public sealed class ImGuiAppTests : IDisposable @@ -152,11 +152,9 @@ public void EnsureWindowPositionIsValid_WithInvalidPosition_MovesToValidPosition // Allow position and size to be set Vector2D finalPosition = offScreenPosition; - Vector2D finalSize = windowSize; mockWindow.SetupSet(w => w.Position = It.IsAny>()) .Callback>(pos => finalPosition = pos); - mockWindow.SetupSet(w => w.Size = It.IsAny>()) - .Callback>(size => finalSize = size); + mockWindow.SetupSet(w => w.Size = It.IsAny>()); mockWindow.SetupSet(w => w.WindowState = It.IsAny()); // Set the mock window directly using internal field @@ -166,191 +164,14 @@ public void EnsureWindowPositionIsValid_WithInvalidPosition_MovesToValidPosition ImGuiApp.EnsureWindowPositionIsValid(); // Verify the window was moved to a valid position - Assert.IsTrue(monitorBounds.Contains(finalPosition), - "Window position should be within monitor bounds"); - - // Verify the window size was preserved (improvement from new logic) - Assert.AreEqual(windowSize, finalSize, "Window size should be preserved when it fits"); - } - - [TestMethod] - public void EnsureWindowPositionIsValid_WithPerformanceOptimization_SkipsUnnecessaryChecks() - { - // Reset state to ensure clean test environment - ResetState(); - - Mock mockWindow = new(); - Mock mockMonitor = new(); - - Rectangle monitorBounds = new(0, 0, 1920, 1080); - mockMonitor.Setup(m => m.Bounds).Returns(monitorBounds); - - Vector2D validPosition = new(100, 100); - Vector2D validSize = new(800, 600); - mockWindow.Setup(w => w.Size).Returns(validSize); - mockWindow.Setup(w => w.Position).Returns(validPosition); - mockWindow.Setup(w => w.Monitor).Returns(mockMonitor.Object); - mockWindow.Setup(w => w.WindowState).Returns(WindowState.Normal); - - ImGuiApp.window = mockWindow.Object; - - // First call should perform validation - ImGuiApp.EnsureWindowPositionIsValid(); - int boundsCallsAfterFirst = mockMonitor.Invocations.Count; - - // Second call with same position/size should skip validation (performance optimization) - ImGuiApp.EnsureWindowPositionIsValid(); - int boundsCallsAfterSecond = mockMonitor.Invocations.Count; - - // Should have made no additional calls on second invocation due to caching - Assert.AreEqual(boundsCallsAfterFirst, boundsCallsAfterSecond, - "Second call should skip validation due to performance optimization"); - } - - [TestMethod] - public void EnsureWindowPositionIsValid_WithPartiallyVisibleWindow_RelocatesWhenInsufficientlyVisible() - { - // Reset state to ensure clean test environment - ResetState(); - - Mock mockWindow = new(); - Mock mockMonitor = new(); - - Rectangle monitorBounds = new(0, 0, 1920, 1080); - mockMonitor.Setup(m => m.Bounds).Returns(monitorBounds); - - // Window with only small corner visible (less than 25% visibility requirement) - Vector2D windowSize = new(800, 600); - Vector2D barelyVisiblePosition = new(1870, 1030); // Only 50x50 pixels visible - mockWindow.Setup(w => w.Size).Returns(windowSize); - mockWindow.Setup(w => w.Position).Returns(barelyVisiblePosition); - mockWindow.Setup(w => w.Monitor).Returns(mockMonitor.Object); - mockWindow.Setup(w => w.WindowState).Returns(WindowState.Normal); - - Vector2D finalPosition = barelyVisiblePosition; - mockWindow.SetupSet(w => w.Position = It.IsAny>()) - .Callback>(pos => finalPosition = pos); - mockWindow.SetupSet(w => w.Size = It.IsAny>()); - mockWindow.SetupSet(w => w.WindowState = It.IsAny()); - - ImGuiApp.window = mockWindow.Object; - ImGuiApp.EnsureWindowPositionIsValid(); - - // Window should be relocated since it's insufficiently visible - Assert.AreNotEqual(barelyVisiblePosition, finalPosition, - "Window should be relocated when insufficiently visible"); - Assert.IsTrue(monitorBounds.Contains(finalPosition), - "Relocated window should be fully within monitor bounds"); - } - - [TestMethod] - public void EnsureWindowPositionIsValid_WithSufficientlyVisibleWindow_LeavesWindowAlone() - { - // Reset state to ensure clean test environment - ResetState(); - - Mock mockWindow = new(); - Mock mockMonitor = new(); - - Rectangle monitorBounds = new(0, 0, 1920, 1080); - mockMonitor.Setup(m => m.Bounds).Returns(monitorBounds); - - // Window with significant visibility (more than 25%) - // Window 800x600 = 480,000 pixels total - // Visible portion: 400x300 = 120,000 pixels (25% exactly) - Vector2D windowSize = new(800, 600); - Vector2D partiallyVisiblePosition = new(1520, 780); // 400x300 pixels visible - mockWindow.Setup(w => w.Size).Returns(windowSize); - mockWindow.Setup(w => w.Position).Returns(partiallyVisiblePosition); - mockWindow.Setup(w => w.Monitor).Returns(mockMonitor.Object); - mockWindow.Setup(w => w.WindowState).Returns(WindowState.Normal); - - mockWindow.SetupSet(w => w.Position = It.IsAny>()); - - ImGuiApp.window = mockWindow.Object; - ImGuiApp.EnsureWindowPositionIsValid(); - - // Window position should not be changed since it has sufficient visibility - mockWindow.VerifySet(w => w.Position = It.IsAny>(), Times.Never, - "Window should not be moved when sufficiently visible"); - } - - [TestMethod] - public void EnsureWindowPositionIsValid_WithOversizedWindow_FitsToMonitor() - { - // Reset state to ensure clean test environment - ResetState(); - - Mock mockWindow = new(); - Mock mockMonitor = new(); - - // Small monitor - Rectangle smallMonitorBounds = new(0, 0, 1024, 768); - mockMonitor.Setup(m => m.Bounds).Returns(smallMonitorBounds); - - // Oversized window completely off-screen - Vector2D oversizedSize = new(2000, 1500); - Vector2D offScreenPosition = new(-2500, -2000); // Completely off-screen - mockWindow.Setup(w => w.Size).Returns(oversizedSize); - mockWindow.Setup(w => w.Position).Returns(offScreenPosition); - mockWindow.Setup(w => w.Monitor).Returns(mockMonitor.Object); - mockWindow.Setup(w => w.WindowState).Returns(WindowState.Normal); - - Vector2D finalSize = oversizedSize; - mockWindow.SetupSet(w => w.Position = It.IsAny>()); - mockWindow.SetupSet(w => w.Size = It.IsAny>()) - .Callback>(size => finalSize = size); - mockWindow.SetupSet(w => w.WindowState = It.IsAny()); - - ImGuiApp.window = mockWindow.Object; - ImGuiApp.EnsureWindowPositionIsValid(); - - // Debug: Check if window was relocated at all - mockWindow.VerifySet(w => w.Position = It.IsAny>(), Times.AtLeastOnce, - "Window should have been relocated when completely off-screen"); - - // Window should be resized to fit monitor (with 100px margin) - Assert.IsTrue(finalSize.X <= smallMonitorBounds.Size.X - 100, - $"Window width {finalSize.X} should be <= {smallMonitorBounds.Size.X - 100} to fit monitor"); - Assert.IsTrue(finalSize.Y <= smallMonitorBounds.Size.Y - 100, - $"Window height {finalSize.Y} should be <= {smallMonitorBounds.Size.Y - 100} to fit monitor"); - Assert.IsTrue(finalSize.X >= 640 && finalSize.Y >= 480, - $"Window size {finalSize} should maintain minimum size of 640x480"); - } - - [TestMethod] - public void ForceWindowPositionValidation_ForcesNextValidation() - { - // Reset state to ensure clean test environment - ResetState(); - - Mock mockWindow = new(); - Mock mockMonitor = new(); - - Rectangle monitorBounds = new(0, 0, 1920, 1080); - mockMonitor.Setup(m => m.Bounds).Returns(monitorBounds); - - Vector2D validPosition = new(100, 100); - Vector2D validSize = new(800, 600); - mockWindow.Setup(w => w.Size).Returns(validSize); - mockWindow.Setup(w => w.Position).Returns(validPosition); - mockWindow.Setup(w => w.Monitor).Returns(mockMonitor.Object); - mockWindow.Setup(w => w.WindowState).Returns(WindowState.Normal); - - ImGuiApp.window = mockWindow.Object; - - // First validation - ImGuiApp.EnsureWindowPositionIsValid(); - int callsAfterFirst = mockMonitor.Invocations.Count; - - // Force validation should make next call perform validation even with same position - ImGuiApp.ForceWindowPositionValidation(); - ImGuiApp.EnsureWindowPositionIsValid(); - int callsAfterForced = mockMonitor.Invocations.Count; - - // Should have made additional calls after forcing validation - Assert.IsTrue(callsAfterForced > callsAfterFirst, - "Forced validation should cause additional monitor access"); + mockWindow.VerifySet(w => w.Position = It.Is>(pos => + monitorBounds.Contains(pos) || + monitorBounds.Contains(pos + windowSize))); + + // Additional verification that the window is now visible + Assert.IsTrue(monitorBounds.Contains(finalPosition) || + monitorBounds.Contains(finalPosition + windowSize), + "Window should be moved to a visible position on the monitor"); } [TestMethod] diff --git a/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs b/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs similarity index 89% rename from tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs rename to ImGuiApp.Test/ImGuiAppWindowManagementTests.cs index 59f9489..bbcbc10 100644 --- a/tests/ImGui.App.Tests/ImGuiAppWindowManagementTests.cs +++ b/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs @@ -2,14 +2,13 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System.Numerics; - +using ktsu.Semantics; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Silk.NET.Windowing; -using ktsu.Semantics.Paths; /// /// Tests for ImGuiApp window management functionality including initialization, configuration validation, and event handling. @@ -48,6 +47,60 @@ public void ValidateConfig_WithValidConfig_DoesNotThrow() } } + [TestMethod] + public void ValidateConfig_WithZeroWidth_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Size = new Vector2(0, 600); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + + [TestMethod] + public void ValidateConfig_WithZeroHeight_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Size = new Vector2(800, 0); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + + [TestMethod] + public void ValidateConfig_WithNegativeWidth_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Size = new Vector2(-100, 600); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + + [TestMethod] + public void ValidateConfig_WithNegativeHeight_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Size = new Vector2(800, -100); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + + [TestMethod] + public void ValidateConfig_WithNegativeXPosition_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Pos = new Vector2(-10, 100); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + + [TestMethod] + public void ValidateConfig_WithNegativeYPosition_ThrowsArgumentException() + { + ImGuiAppConfig config = TestHelpers.CreateTestConfig(); + config.InitialWindowState.Pos = new Vector2(100, -10); + + Assert.ThrowsExactly(() => ImGuiApp.ValidateConfig(config)); + } + [TestMethod] public void AdjustConfigForStartup_WithMinimizedState_ConvertsToNormal() { diff --git a/tests/ImGui.App.Tests/ImGuiControllerComponentTests.cs b/ImGuiApp.Test/ImGuiControllerComponentTests.cs similarity index 99% rename from tests/ImGui.App.Tests/ImGuiControllerComponentTests.cs rename to ImGuiApp.Test/ImGuiControllerComponentTests.cs index bf1dd33..9a02c96 100644 --- a/tests/ImGui.App.Tests/ImGuiControllerComponentTests.cs +++ b/ImGuiApp.Test/ImGuiControllerComponentTests.cs @@ -2,11 +2,11 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System; using Hexa.NET.ImGui; -using ktsu.ImGui.App.ImGuiController; +using ktsu.ImGuiApp.ImGuiController; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Silk.NET.Windowing; diff --git a/tests/ImGui.App.Tests/MockGL.cs b/ImGuiApp.Test/MockGL.cs similarity index 97% rename from tests/ImGui.App.Tests/MockGL.cs rename to ImGuiApp.Test/MockGL.cs index 50727d8..0d3a124 100644 --- a/tests/ImGui.App.Tests/MockGL.cs +++ b/ImGuiApp.Test/MockGL.cs @@ -2,9 +2,9 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; -using ktsu.ImGui.App.ImGuiController; +using ktsu.ImGuiApp.ImGuiController; using Silk.NET.Maths; using Silk.NET.OpenGL; using Color = System.Drawing.Color; diff --git a/tests/ImGui.App.Tests/PidFrameLimiterTests.cs b/ImGuiApp.Test/PidFrameLimiterTests.cs similarity index 99% rename from tests/ImGui.App.Tests/PidFrameLimiterTests.cs rename to ImGuiApp.Test/PidFrameLimiterTests.cs index 7560f18..71dfd8b 100644 --- a/tests/ImGui.App.Tests/PidFrameLimiterTests.cs +++ b/ImGuiApp.Test/PidFrameLimiterTests.cs @@ -2,12 +2,10 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; -using ktsu.ImGui.App; - /// /// Tests for PidFrameLimiter class covering frame rate limiting, PID controller logic, and auto-tuning functionality. /// diff --git a/tests/ImGui.App.Tests/PlatformSpecificTests.cs b/ImGuiApp.Test/PlatformSpecificTests.cs similarity index 88% rename from tests/ImGui.App.Tests/PlatformSpecificTests.cs rename to ImGuiApp.Test/PlatformSpecificTests.cs index 6c74a25..aaba07a 100644 --- a/tests/ImGui.App.Tests/PlatformSpecificTests.cs +++ b/ImGuiApp.Test/PlatformSpecificTests.cs @@ -2,10 +2,9 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using Microsoft.VisualStudio.TestTools.UnitTesting; -using ktsu.ImGui.App.ImGuiController; /// /// Tests for platform-specific functionality including DPI awareness, native methods, and GDI+ helpers. @@ -69,7 +68,7 @@ public void NativeMethods_IsInternalClass() [TestMethod] public void UniformFieldInfo_IsStruct() { - Type type = typeof(UniformFieldInfo); + Type type = typeof(ImGuiController.UniformFieldInfo); Assert.IsTrue(type.IsValueType); Assert.IsFalse(type.IsClass); } @@ -78,7 +77,7 @@ public void UniformFieldInfo_IsStruct() public void UniformFieldInfo_HasExpectedFields() { // Test UniformFieldInfo through direct access using internal visibility - UniformFieldInfo uniformInfo = new() + ImGuiController.UniformFieldInfo uniformInfo = new() { Location = 1, Name = "test", @@ -102,7 +101,7 @@ public void UniformFieldInfo_HasExpectedFields() [TestMethod] public void Shader_IsInternalClass() { - Type shaderType = typeof(Shader); + Type shaderType = typeof(ImGuiController.Shader); Assert.IsTrue(shaderType.IsClass); Assert.IsFalse(shaderType.IsPublic); } @@ -110,7 +109,7 @@ public void Shader_IsInternalClass() [TestMethod] public void Texture_IsInternalClass() { - Type textureType = typeof(Texture); + Type textureType = typeof(ImGuiController.Texture); Assert.IsTrue(textureType.IsClass); Assert.IsFalse(textureType.IsPublic); } @@ -122,7 +121,7 @@ public void Texture_IsInternalClass() [TestMethod] public void IGL_IsInterface() { - Type iglType = typeof(IGL); + Type iglType = typeof(ImGuiController.IGL); Assert.IsTrue(iglType.IsInterface); Assert.IsTrue(iglType.IsPublic); } @@ -130,7 +129,7 @@ public void IGL_IsInterface() [TestMethod] public void IGL_InheritsFromIDisposable() { - Type iglType = typeof(IGL); + Type iglType = typeof(ImGuiController.IGL); Assert.IsTrue(typeof(IDisposable).IsAssignableFrom(iglType)); } @@ -160,8 +159,8 @@ public void IOpenGLProvider_InheritsFromIDisposable() [TestMethod] public void GLWrapper_ImplementsIGL() { - Type wrapperType = typeof(GLWrapper); - Assert.IsTrue(typeof(IGL).IsAssignableFrom(wrapperType)); + Type wrapperType = typeof(ImGuiController.GLWrapper); + Assert.IsTrue(typeof(ImGuiController.IGL).IsAssignableFrom(wrapperType)); } #endregion diff --git a/tests/ImGui.App.Tests/TestGL.cs b/ImGuiApp.Test/TestGL.cs similarity index 95% rename from tests/ImGui.App.Tests/TestGL.cs rename to ImGuiApp.Test/TestGL.cs index f4deac0..30c1b89 100644 --- a/tests/ImGui.App.Tests/TestGL.cs +++ b/ImGuiApp.Test/TestGL.cs @@ -2,9 +2,9 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; -using ktsu.ImGui.App.ImGuiController; +using ktsu.ImGuiApp.ImGuiController; using Silk.NET.Maths; using Silk.NET.OpenGL; using Color = System.Drawing.Color; diff --git a/tests/ImGui.App.Tests/TestHelpers.cs b/ImGuiApp.Test/TestHelpers.cs similarity index 99% rename from tests/ImGui.App.Tests/TestHelpers.cs rename to ImGuiApp.Test/TestHelpers.cs index 3b5a566..023eb2e 100644 --- a/tests/ImGui.App.Tests/TestHelpers.cs +++ b/ImGuiApp.Test/TestHelpers.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; using System.Numerics; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/tests/ImGui.App.Tests/TestOpenGLProvider.cs b/ImGuiApp.Test/TestOpenGLProvider.cs similarity index 90% rename from tests/ImGui.App.Tests/TestOpenGLProvider.cs rename to ImGuiApp.Test/TestOpenGLProvider.cs index 2d795ff..b748f9d 100644 --- a/tests/ImGui.App.Tests/TestOpenGLProvider.cs +++ b/ImGuiApp.Test/TestOpenGLProvider.cs @@ -2,9 +2,9 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.Tests; +namespace ktsu.ImGuiApp.Test; -using ktsu.ImGui.App.ImGuiController; +using ktsu.ImGuiApp.ImGuiController; using Silk.NET.OpenGL; /// diff --git a/ImGuiApp.sln b/ImGuiApp.sln new file mode 100644 index 0000000..ef884a4 --- /dev/null +++ b/ImGuiApp.sln @@ -0,0 +1,65 @@ +īģŋ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiApp", "ImGuiApp\ImGuiApp.csproj", "{62D571FE-4493-467A-A8BD-212BA2DEB1AB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiAppDemo", "ImGuiAppDemo\ImGuiAppDemo.csproj", "{7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiApp.Test", "ImGuiApp.Test\ImGuiApp.Test.csproj", "{F9F2FA6F-5D7D-4612-B855-7E23346A9835}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|x64.Build.0 = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Debug|x86.Build.0 = Debug|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|Any CPU.Build.0 = Release|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|x64.ActiveCfg = Release|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|x64.Build.0 = Release|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|x86.ActiveCfg = Release|Any CPU + {62D571FE-4493-467A-A8BD-212BA2DEB1AB}.Release|x86.Build.0 = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|x64.Build.0 = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Debug|x86.Build.0 = Debug|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|Any CPU.Build.0 = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|x64.ActiveCfg = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|x64.Build.0 = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|x86.ActiveCfg = Release|Any CPU + {7588346A-C3CC-4F7E-AE44-E9905DB0AFA4}.Release|x86.Build.0 = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|x64.ActiveCfg = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|x64.Build.0 = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|x86.ActiveCfg = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Debug|x86.Build.0 = Debug|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|Any CPU.Build.0 = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|x64.ActiveCfg = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|x64.Build.0 = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|x86.ActiveCfg = Release|Any CPU + {F9F2FA6F-5D7D-4612-B855-7E23346A9835}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {14515AC1-A74C-4CBB-98F2-9DA660E967EF} + EndGlobalSection +EndGlobal diff --git a/ImGui.App/DebugLogger.cs b/ImGuiApp/DebugLogger.cs similarity index 92% rename from ImGui.App/DebugLogger.cs rename to ImGuiApp/DebugLogger.cs index e6b86c3..b1447fa 100644 --- a/ImGui.App/DebugLogger.cs +++ b/ImGuiApp/DebugLogger.cs @@ -2,10 +2,10 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; +using ktsu.Extensions; +using ktsu.Semantics; /// /// Simple file logger for debugging crashes diff --git a/ImGui.App/FontAppearance.cs b/ImGuiApp/FontAppearance.cs similarity index 98% rename from ImGui.App/FontAppearance.cs rename to ImGuiApp/FontAppearance.cs index 84c653e..54fb940 100644 --- a/ImGui.App/FontAppearance.cs +++ b/ImGuiApp/FontAppearance.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using Hexa.NET.ImGui; diff --git a/ImGui.App/FontHelper.cs b/ImGuiApp/FontHelper.cs similarity index 99% rename from ImGui.App/FontHelper.cs rename to ImGuiApp/FontHelper.cs index f3153c0..fac5ac8 100644 --- a/ImGui.App/FontHelper.cs +++ b/ImGuiApp/FontHelper.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System; using System.Runtime.InteropServices; diff --git a/ImGui.App/ForceDpiAware.cs b/ImGuiApp/ForceDpiAware.cs similarity index 94% rename from ImGui.App/ForceDpiAware.cs rename to ImGuiApp/ForceDpiAware.cs index 5fe76c0..3d2183f 100644 --- a/ImGui.App/ForceDpiAware.cs +++ b/ImGuiApp/ForceDpiAware.cs @@ -4,7 +4,7 @@ #pragma warning disable IDE0078 // Use pattern matching -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.ComponentModel; using System.Globalization; @@ -21,50 +21,12 @@ public static partial class ForceDpiAware /// /// Marks the application as DPI-Aware when running on the Windows operating system. - /// Uses modern DPI awareness APIs for better compatibility with windowing libraries. /// public static void Windows() { // Make process DPI aware for proper window sizing on high-res screens. - // Use the most modern API available for better compatibility with GLFW and other windowing libraries. - - if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14393)) // Windows 10 Version 1607 - { - // Try the modern DPI awareness context API first (recommended) - try - { - nint result = NativeMethods.SetProcessDpiAwarenessContext(NativeMethods.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - if (result != IntPtr.Zero) - { - return; // Success - } - } - catch (EntryPointNotFoundException) - { - // SetProcessDpiAwarenessContext not available, fall back to older API - } - } - - if (OperatingSystem.IsWindowsVersionAtLeast(6, 3)) // Windows 8.1 - { - // Fall back to SetProcessDpiAwareness - try - { - int result = NativeMethods.SetProcessDpiAwareness(NativeMethods.ProcessDpiAwareness.ProcessPerMonitorDpiAware); - if (result == 0) // S_OK - { - return; // Success - } - } - catch (EntryPointNotFoundException) - { - // SetProcessDpiAwareness not available, fall back to oldest API - } - } - - if (OperatingSystem.IsWindowsVersionAtLeast(6)) // Windows Vista + if (OperatingSystem.IsWindowsVersionAtLeast(6)) { - // Fall back to the legacy API as last resort NativeMethods.SetProcessDPIAware(); } } diff --git a/ImGui.App/GdiPlusHelper.cs b/ImGuiApp/GdiPlusHelper.cs similarity index 98% rename from ImGui.App/GdiPlusHelper.cs rename to ImGuiApp/GdiPlusHelper.cs index adf7ca8..ac09fea 100644 --- a/ImGui.App/GdiPlusHelper.cs +++ b/ImGuiApp/GdiPlusHelper.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Runtime.Versioning; diff --git a/ImGui.App/ImGuiApp.cs b/ImGuiApp/ImGuiApp.cs similarity index 84% rename from ImGui.App/ImGuiApp.cs rename to ImGuiApp/ImGuiApp.cs index 2a9903f..1466f28 100644 --- a/ImGui.App/ImGuiApp.cs +++ b/ImGuiApp/ImGuiApp.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Buffers; using System.Collections.Concurrent; @@ -11,11 +11,11 @@ namespace ktsu.ImGui.App; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Hexa.NET.ImGui; -using ktsu.ImGui.App.ImGuiController; +using ktsu.Extensions; +using ktsu.ImGuiApp.ImGuiController; using ktsu.Invoker; using ktsu.ScopedAction; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; +using ktsu.Semantics; using Silk.NET.Input; using Silk.NET.OpenGL; using Silk.NET.Windowing; @@ -101,9 +101,16 @@ public static ImGuiAppWindowState WindowState /// internal static void OnUserInput() => lastInputTime = DateTime.UtcNow; - internal static bool showImGuiMetrics; - internal static bool showImGuiDemo; - internal static bool showPerformanceMonitor; + internal static bool isImGuiMetricsVisible; + internal static bool isImGuiDemoVisible; + internal static bool isImGuiStyleEditorVisible; + internal static bool isPerformanceMonitorVisible; + + // Track whether we need to focus windows on their first frame + internal static bool shouldFocusImGuiDemo; + internal static bool shouldFocusImGuiMetrics; + internal static bool shouldFocusImGuiStyleEditor; + internal static bool shouldFocusPerformanceMonitor; // Performance monitoring data structures internal static readonly Queue performanceFrameTimes = new(); @@ -118,9 +125,8 @@ public static ImGuiAppWindowState WindowState public static float ScaleFactor { get; private set; } = 1; /// - /// Gets or sets the global UI scale factor for accessibility. - /// This is a user-adjustable scale that is separate from DPI scaling. - /// Default is 1.0 (100%). Valid range is 0.5 to 3.0. + /// Gets the global UI scale factor for accessibility. + /// This scale is applied on top of DPI scaling and affects all UI elements. /// public static float GlobalScale { get; private set; } = 1.0f; @@ -157,6 +163,54 @@ public static void SetGlobalScale(float scale) Config.OnGlobalScaleChanged?.Invoke(scale); } + /// + /// Opens the ImGui Demo window. + /// + public static void ShowImGuiDemo() + { + if (!isImGuiDemoVisible) + { + shouldFocusImGuiDemo = true; + } + isImGuiDemoVisible = true; + } + + /// + /// Opens the ImGui Metrics window. + /// + public static void ShowImGuiMetrics() + { + if (!isImGuiMetricsVisible) + { + shouldFocusImGuiMetrics = true; + } + isImGuiMetricsVisible = true; + } + + /// + /// Opens the ImGui Style Editor window. + /// + public static void ShowImGuiStyleEditor() + { + if (!isImGuiStyleEditorVisible) + { + shouldFocusImGuiStyleEditor = true; + } + isImGuiStyleEditorVisible = true; + } + + /// + /// Opens the Performance Monitor window. + /// + public static void ShowPerformanceMonitor() + { + if (!isPerformanceMonitorVisible) + { + shouldFocusPerformanceMonitor = true; + } + isPerformanceMonitorVisible = true; + } + internal static ImGuiAppConfig Config { get; set; } = new(); internal static void InitializeWindow(ImGuiAppConfig config) @@ -271,10 +325,6 @@ internal static void SetupWindowResizeHandler(ImGuiAppConfig config) CaptureWindowNormalState(); UpdateDpiScale(); CheckAndHandleContextChange(); - - // Force validation on next update cycle since window size changed - ForceWindowPositionValidation(); - config.OnMoveOrResize?.Invoke(); }; } @@ -286,10 +336,6 @@ internal static void SetupWindowMoveHandler(ImGuiAppConfig config) CaptureWindowNormalState(); UpdateDpiScale(); CheckAndHandleContextChange(); - - // Force validation on next update cycle since window may have moved to different monitor - ForceWindowPositionValidation(); - config.OnMoveOrResize?.Invoke(); }; } @@ -369,6 +415,8 @@ internal static void SetupWindowUpdateHandler(ImGuiAppConfig config) UpdateWindowPerformance(); UpdatePerformanceMonitoring((float)delta); + // SetupContext prepares ImGui context for this frame + controller?.SetupContext(); controller?.Update((float)delta); config.OnUpdate?.Invoke((float)delta); Invoker.DoInvokes(); @@ -419,7 +467,6 @@ internal static void RenderWithScaling(Action renderAction) { FindBestFontForAppearance(FontAppearance.DefaultFontName, FontAppearance.DefaultFontPointSize, out float bestFontSize); float scaleRatio = bestFontSize / FontAppearance.DefaultFontPointSize; - // Apply both DPI scale and global accessibility scale float combinedScale = scaleRatio * GlobalScale; using (new UIScaler(combinedScale)) { @@ -570,6 +617,16 @@ internal static void AdjustConfigForStartup(ImGuiAppConfig config) internal static void ValidateConfig(ImGuiAppConfig config) { + if (config.InitialWindowState.Size.X <= 0 || config.InitialWindowState.Size.Y <= 0) + { + throw new ArgumentException("Initial window size must be greater than zero.", nameof(config)); + } + + if (config.InitialWindowState.Pos.X < 0 || config.InitialWindowState.Pos.Y < 0) + { + throw new ArgumentException("Initial window position must be non-negative.", nameof(config)); + } + if (!string.IsNullOrEmpty(config.IconPath) && !File.Exists(config.IconPath)) { throw new FileNotFoundException("Icon file not found.", config.IconPath); @@ -676,220 +733,26 @@ internal static ImFontPtr FindBestFontForAppearance(string name, int sizePoints, return fonts[fontIndex]; } - // Cache to avoid checking every frame - private static Silk.NET.Maths.Vector2D? lastCheckedWindowPosition; - private static Silk.NET.Maths.Vector2D? lastCheckedWindowSize; - private static bool needsPositionValidation = true; - - /// - /// Ensures the window is positioned on a visible monitor. This method provides improved - /// multi-monitor support, better visibility detection, and performance optimizations. - /// internal static void EnsureWindowPositionIsValid() { - // Early exit for invalid states - if (window is null || window.WindowState == Silk.NET.Windowing.WindowState.Minimized) - { - return; - } - - // Performance optimization: only check when position/size changes or forced - if (!needsPositionValidation && - lastCheckedWindowPosition == window.Position && - lastCheckedWindowSize == window.Size) - { - return; - } - - try - { - // Update cache - lastCheckedWindowPosition = window.Position; - lastCheckedWindowSize = window.Size; - needsPositionValidation = false; - - // Get all available monitors for better multi-monitor support - IMonitor[] availableMonitors = GetAvailableMonitors(); - if (availableMonitors.Length == 0) - { - DebugLogger.Log("EnsureWindowPositionIsValid: No monitors available, skipping validation"); - return; - } - - // Check if window has sufficient visibility on any monitor - IMonitor? bestMonitor = FindBestMonitorForWindow(window, availableMonitors); - - if (bestMonitor is null) - { - // Window is not sufficiently visible on any monitor - relocate it - IMonitor targetMonitor = GetPrimaryMonitor(availableMonitors) ?? availableMonitors[0]; - RelocateWindowToMonitor(window, targetMonitor); - DebugLogger.Log($"EnsureWindowPositionIsValid: Relocated window to monitor bounds {targetMonitor.Bounds}"); - } - else if (window.Monitor != bestMonitor) - { - DebugLogger.Log($"EnsureWindowPositionIsValid: Window is visible on monitor {bestMonitor.Bounds}"); - } - } - catch (InvalidOperationException ex) - { - DebugLogger.Log($"EnsureWindowPositionIsValid: Error during validation - {ex.Message}"); - // Don't crash the application, just log the error - } - catch (NullReferenceException ex) - { - DebugLogger.Log($"EnsureWindowPositionIsValid: Error during validation - {ex.Message}"); - // Don't crash the application, just log the error - } - } - - /// - /// Gets all available monitors from the windowing system. - /// - private static IMonitor[] GetAvailableMonitors() - { - try - { - // Try to get monitors from the current window's context first - if (window?.Monitor?.Bounds is not null) - { - // For Silk.NET, we need to work with what's available - // In many cases, we only have access to the current monitor - return [window.Monitor]; - } - - // Fallback: if no monitor available, return empty array - return []; - } - catch (InvalidOperationException) - { - return []; - } - catch (NullReferenceException) - { - return []; - } - } - - /// - /// Finds the monitor where the window has the most visibility. - /// Returns null if the window is not sufficiently visible on any monitor. - /// - private static IMonitor? FindBestMonitorForWindow(IWindow window, IMonitor[] monitors) - { - if (monitors.Length == 0) + if (window?.Monitor is not null && window.WindowState is not Silk.NET.Windowing.WindowState.Minimized) { - return null; - } - - IMonitor? bestMonitor = null; - int maxVisibleArea = 0; - - // Window rectangle - Silk.NET.Maths.Rectangle windowRect = new( - window.Position.X, - window.Position.Y, - window.Size.X, - window.Size.Y); + Silk.NET.Maths.Rectangle bounds = window.Monitor.Bounds; + bool onScreen = bounds.Contains(window.Position) || + bounds.Contains(window.Position + new Silk.NET.Maths.Vector2D(window.Size.X, 0)) || + bounds.Contains(window.Position + new Silk.NET.Maths.Vector2D(0, window.Size.Y)) || + bounds.Contains(window.Position + new Silk.NET.Maths.Vector2D(window.Size.X, window.Size.Y)); - foreach (IMonitor monitor in monitors) - { - // Calculate intersection area with this monitor - Silk.NET.Maths.Rectangle intersection = CalculateRectangleIntersection(windowRect, monitor.Bounds); - int visibleArea = intersection.Size.X * intersection.Size.Y; - - if (visibleArea > maxVisibleArea) + if (!onScreen) { - maxVisibleArea = visibleArea; - bestMonitor = monitor; + // If the window is not on a monitor, move it to the primary monitor + ImGuiAppWindowState defaultWindowState = new(); + System.Numerics.Vector2 halfSize = defaultWindowState.Size / 2; + window.Size = new((int)defaultWindowState.Size.X, (int)defaultWindowState.Size.Y); + window.Position = window.Monitor.Bounds.Center - new Silk.NET.Maths.Vector2D((int)halfSize.X, (int)halfSize.Y); + window.WindowState = defaultWindowState.LayoutState; } } - - // Require at least 25% of the window to be visible (or minimum 100x100 pixels) - int windowArea = windowRect.Size.X * windowRect.Size.Y; - int minimumVisibleArea = Math.Max(10000, windowArea / 4); // 100x100 or 25% of window - - return maxVisibleArea >= minimumVisibleArea ? bestMonitor : null; - } - - /// - /// Gets the primary monitor from the available monitors. - /// - private static IMonitor? GetPrimaryMonitor(IMonitor[] monitors) => - // In Silk.NET, we typically work with the monitor associated with the window - // For now, we'll use the first monitor as primary - monitors.Length > 0 ? monitors[0] : null; - - /// - /// Relocates the window to the specified monitor, preserving the current window size - /// unless it's too large for the target monitor. - /// - private static void RelocateWindowToMonitor(IWindow window, IMonitor monitor) - { - Silk.NET.Maths.Rectangle monitorBounds = monitor.Bounds; - Silk.NET.Maths.Vector2D currentSize = window.Size; - - // Preserve current size if it fits, otherwise use default size - Silk.NET.Maths.Vector2D newSize; - if (currentSize.X <= monitorBounds.Size.X - 100 && currentSize.Y <= monitorBounds.Size.Y - 100) - { - // Current size fits with some margin - keep it - newSize = currentSize; - } - else - { - // Current size is too large - use default size or fit to monitor - ImGuiAppWindowState defaultState = new(); - int maxWidth = Math.Min((int)defaultState.Size.X, monitorBounds.Size.X - 100); - int maxHeight = Math.Min((int)defaultState.Size.Y, monitorBounds.Size.Y - 100); - newSize = new(Math.Max(640, maxWidth), Math.Max(480, maxHeight)); // Minimum 640x480 - } - - // Center the window on the target monitor - Silk.NET.Maths.Vector2D halfSize = new(newSize.X / 2, newSize.Y / 2); - Silk.NET.Maths.Vector2D centerPosition = monitorBounds.Center - halfSize; - - // Ensure the window is fully within the monitor bounds - int posX = Math.Max(monitorBounds.Origin.X, Math.Min(centerPosition.X, monitorBounds.Origin.X + monitorBounds.Size.X - newSize.X)); - int posY = Math.Max(monitorBounds.Origin.Y, Math.Min(centerPosition.Y, monitorBounds.Origin.Y + monitorBounds.Size.Y - newSize.Y)); - - // Apply the new position and size - window.Size = newSize; - window.Position = new(posX, posY); - window.WindowState = Silk.NET.Windowing.WindowState.Normal; - - DebugLogger.Log($"EnsureWindowPositionIsValid: Repositioned window to ({posX}, {posY}) with size ({newSize.X}, {newSize.Y})"); - } - - /// - /// Calculates the intersection rectangle of two rectangles. - /// - private static Silk.NET.Maths.Rectangle CalculateRectangleIntersection( - Silk.NET.Maths.Rectangle rect1, - Silk.NET.Maths.Rectangle rect2) - { - int left = Math.Max(rect1.Origin.X, rect2.Origin.X); - int top = Math.Max(rect1.Origin.Y, rect2.Origin.Y); - int right = Math.Min(rect1.Origin.X + rect1.Size.X, rect2.Origin.X + rect2.Size.X); - int bottom = Math.Min(rect1.Origin.Y + rect1.Size.Y, rect2.Origin.Y + rect2.Size.Y); - - // If rectangles don't intersect, return empty rectangle - if (left >= right || top >= bottom) - { - return new Silk.NET.Maths.Rectangle(0, 0, 0, 0); - } - - return new Silk.NET.Maths.Rectangle(left, top, right - left, bottom - top); - } - - /// - /// Forces the window position validation to run on the next update cycle. - /// Call this when monitor configuration may have changed. - /// - internal static void ForceWindowPositionValidation() - { - needsPositionValidation = true; - DebugLogger.Log("EnsureWindowPositionIsValid: Forced validation requested"); } /// @@ -904,10 +767,12 @@ internal static void RenderAppMenu(Action? menuDelegate) { menuDelegate(); - if (ImGui.BeginMenu("Accessibility")) + if (ImGui.BeginMenu("Scale")) { - ImGui.Text("UI Scale:"); - ImGui.Separator(); + if (ImGui.MenuItem("50%", "", Math.Abs(GlobalScale - 0.5f) < 0.01f)) + { + SetGlobalScale(0.5f); + } if (ImGui.MenuItem("75%", "", Math.Abs(GlobalScale - 0.75f) < 0.01f)) { @@ -944,19 +809,54 @@ internal static void RenderAppMenu(Action? menuDelegate) if (ImGui.BeginMenu("Debug")) { - if (ImGui.MenuItem("Show ImGui Demo", "", showImGuiDemo)) + if (ImGui.MenuItem("Show ImGui Demo", "", isImGuiDemoVisible)) { - showImGuiDemo = !showImGuiDemo; + if (!isImGuiDemoVisible) + { + ShowImGuiDemo(); + } + else + { + isImGuiDemoVisible = false; + } } - if (ImGui.MenuItem("Show ImGui Metrics", "", showImGuiMetrics)) + if (ImGui.MenuItem("Show ImGui Metrics", "", isImGuiMetricsVisible)) { - showImGuiMetrics = !showImGuiMetrics; + if (!isImGuiMetricsVisible) + { + ShowImGuiMetrics(); + } + else + { + isImGuiMetricsVisible = false; + } + } + + if (ImGui.MenuItem("Show Style Editor", "", isImGuiStyleEditorVisible)) + { + if (!isImGuiStyleEditorVisible) + { + ShowImGuiStyleEditor(); + } + else + { + isImGuiStyleEditorVisible = false; + } } - if (ImGui.MenuItem("Show Performance Monitor", "", showPerformanceMonitor)) + ImGui.Separator(); + + if (ImGui.MenuItem("Show Performance Monitor", "", isPerformanceMonitorVisible)) { - showPerformanceMonitor = !showPerformanceMonitor; + if (!isPerformanceMonitorVisible) + { + ShowPerformanceMonitor(); + } + else + { + isPerformanceMonitorVisible = false; + } } ImGui.EndMenu(); @@ -979,7 +879,7 @@ internal static void RenderWindowContents(Action? tickDelegate, float dt) ImGui.SetNextWindowPos(ImGui.GetMainViewport().WorkPos); ImGuiStylePtr style = ImGui.GetStyle(); System.Numerics.Vector4 borderColor = style.Colors[(int)ImGuiCol.Border]; - if (ImGui.Begin("##mainWindow", ref b, ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings)) + if (ImGui.Begin("##mainWindow", ref b, ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoBringToFrontOnFocus)) { style.Colors[(int)ImGuiCol.Border] = borderColor; tickDelegate?.Invoke(dt); @@ -987,14 +887,57 @@ internal static void RenderWindowContents(Action? tickDelegate, float dt) ImGui.End(); - if (showImGuiDemo) + if (isImGuiDemoVisible) { - ImGui.ShowDemoWindow(ref showImGuiDemo); + // Position the demo window to ensure it's visible and accessible + ImGui.SetNextWindowPos(new System.Numerics.Vector2(50, 50), ImGuiCond.FirstUseEver); + ImGui.SetNextWindowSize(new System.Numerics.Vector2(800, 600), ImGuiCond.FirstUseEver); + + // Focus on first frame after opening for good UX + if (shouldFocusImGuiDemo) + { + ImGui.SetNextWindowFocus(); + shouldFocusImGuiDemo = false; + } + + ImGui.ShowDemoWindow(ref isImGuiDemoVisible); } - if (showImGuiMetrics) + if (isImGuiMetricsVisible) { - ImGui.ShowMetricsWindow(ref showImGuiMetrics); + // Position the metrics window to ensure it's visible and accessible + ImGui.SetNextWindowPos(new System.Numerics.Vector2(100, 100), ImGuiCond.FirstUseEver); + ImGui.SetNextWindowSize(new System.Numerics.Vector2(600, 500), ImGuiCond.FirstUseEver); + + // Focus on first frame after opening for good UX + if (shouldFocusImGuiMetrics) + { + ImGui.SetNextWindowFocus(); + shouldFocusImGuiMetrics = false; + } + + ImGui.ShowMetricsWindow(ref isImGuiMetricsVisible); + } + + if (isImGuiStyleEditorVisible) + { + // Position and configure the style editor window for better accessibility + ImGui.SetNextWindowPos(new System.Numerics.Vector2(150, 150), ImGuiCond.FirstUseEver); + ImGui.SetNextWindowSize(new System.Numerics.Vector2(500, 400), ImGuiCond.FirstUseEver); + + // Focus on first frame after opening for good UX + if (shouldFocusImGuiStyleEditor) + { + ImGui.SetNextWindowFocus(); + shouldFocusImGuiStyleEditor = false; + } + + if (ImGui.Begin("Style Editor", ref isImGuiStyleEditorVisible)) + { + ImGui.ShowStyleEditor(); + } + + ImGui.End(); } } @@ -1454,9 +1397,16 @@ internal static void Reset() IsIdle = false; lastInputTime = DateTime.UtcNow; targetFrameTimeMs = 1000.0 / 30.0; - showImGuiMetrics = false; - showImGuiDemo = false; - showPerformanceMonitor = false; + isImGuiMetricsVisible = false; + isImGuiDemoVisible = false; + isImGuiStyleEditorVisible = false; + isPerformanceMonitorVisible = false; + + shouldFocusImGuiDemo = false; + shouldFocusImGuiMetrics = false; + shouldFocusImGuiStyleEditor = false; + shouldFocusPerformanceMonitor = false; + performanceFrameTimes.Clear(); performanceFrameTimeSum = 0; performanceFpsHistory.Clear(); @@ -1576,12 +1526,23 @@ internal static void UpdatePerformanceMonitoring(float dt) /// internal static void RenderPerformanceMonitor() { - if (!showPerformanceMonitor) + if (!isPerformanceMonitorVisible) { return; } - if (ImGui.Begin("Performance Monitor", ref showPerformanceMonitor)) + // Position the performance monitor window to ensure it's visible and accessible + ImGui.SetNextWindowPos(new System.Numerics.Vector2(200, 200), ImGuiCond.FirstUseEver); + ImGui.SetNextWindowSize(new System.Numerics.Vector2(500, 350), ImGuiCond.FirstUseEver); + + // Focus on first frame after opening for good UX + if (shouldFocusPerformanceMonitor) + { + ImGui.SetNextWindowFocus(); + shouldFocusPerformanceMonitor = false; + } + + if (ImGui.Begin("Performance Monitor", ref isPerformanceMonitorVisible)) { ImGui.TextWrapped("This window shows the current performance state and throttling behavior."); ImGui.Separator(); diff --git a/ImGui.App/ImGui.App.csproj b/ImGuiApp/ImGuiApp.csproj similarity index 65% rename from ImGui.App/ImGui.App.csproj rename to ImGuiApp/ImGuiApp.csproj index 51f151e..e9350d8 100644 --- a/ImGui.App/ImGui.App.csproj +++ b/ImGuiApp/ImGuiApp.csproj @@ -1,29 +1,26 @@ - - - - - - - + true - net8.0;net9.0;net7.0 + $(NoWarn);CA5392; + - - + - - + + + - + + + @@ -41,6 +38,4 @@ - - diff --git a/ImGui.App/ImGuiAppConfig.cs b/ImGuiApp/ImGuiAppConfig.cs similarity index 95% rename from ImGui.App/ImGuiAppConfig.cs rename to ImGuiApp/ImGuiAppConfig.cs index bf1c602..deab217 100644 --- a/ImGui.App/ImGuiAppConfig.cs +++ b/ImGuiApp/ImGuiAppConfig.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Resources; using ktsu.ScopedAction; @@ -71,8 +71,7 @@ public class ImGuiAppConfig public Action OnMoveOrResize { get; init; } = () => { }; /// - /// Gets or sets the action to be performed when the global UI scale changes. - /// The parameter is the new scale factor (e.g., 1.0 for 100%, 1.5 for 150%). + /// Gets or sets the action to be performed when the global scale factor changes. /// This can be used to persist the scale preference. /// public Action OnGlobalScaleChanged { get; init; } = (scale) => { }; diff --git a/ImGui.App/ImGuiAppPerformanceSettings.cs b/ImGuiApp/ImGuiAppPerformanceSettings.cs similarity index 98% rename from ImGui.App/ImGuiAppPerformanceSettings.cs rename to ImGuiApp/ImGuiAppPerformanceSettings.cs index ec0918c..2200c83 100644 --- a/ImGui.App/ImGuiAppPerformanceSettings.cs +++ b/ImGuiApp/ImGuiAppPerformanceSettings.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; /// /// Represents performance settings for throttled rendering to save system resources. diff --git a/ImGui.App/ImGuiAppTextureInfo.cs b/ImGuiApp/ImGuiAppTextureInfo.cs similarity index 94% rename from ImGui.App/ImGuiAppTextureInfo.cs rename to ImGuiApp/ImGuiAppTextureInfo.cs index 33b26b9..88960af 100644 --- a/ImGui.App/ImGuiAppTextureInfo.cs +++ b/ImGuiApp/ImGuiAppTextureInfo.cs @@ -2,9 +2,9 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using Hexa.NET.ImGui; -using ktsu.Semantics.Paths; +using ktsu.Semantics; /// /// Represents information about a texture, including its file path, texture ID, width, and height. diff --git a/ImGui.App/ImGuiAppWindowState.cs b/ImGuiApp/ImGuiAppWindowState.cs similarity index 96% rename from ImGui.App/ImGuiAppWindowState.cs rename to ImGuiApp/ImGuiAppWindowState.cs index 920a894..1cde889 100644 --- a/ImGui.App/ImGuiAppWindowState.cs +++ b/ImGuiApp/ImGuiAppWindowState.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Numerics; using Silk.NET.Windowing; diff --git a/ImGui.App/ImGuiController/GLWrapper.cs b/ImGuiApp/ImGuiController/GLWrapper.cs similarity index 98% rename from ImGui.App/ImGuiController/GLWrapper.cs rename to ImGuiApp/ImGuiController/GLWrapper.cs index 7d69bbe..5bfa261 100644 --- a/ImGui.App/ImGuiController/GLWrapper.cs +++ b/ImGuiApp/ImGuiController/GLWrapper.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using Silk.NET.Maths; using Silk.NET.OpenGL; diff --git a/ImGui.App/ImGuiController/IGL.cs b/ImGuiApp/ImGuiController/IGL.cs similarity index 98% rename from ImGui.App/ImGuiController/IGL.cs rename to ImGuiApp/ImGuiController/IGL.cs index 105d9d0..696d4df 100644 --- a/ImGui.App/ImGuiController/IGL.cs +++ b/ImGuiApp/ImGuiController/IGL.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using Silk.NET.Maths; using Silk.NET.OpenGL; diff --git a/ImGui.App/ImGuiController/IOpenGLFactory.cs b/ImGuiApp/ImGuiController/IOpenGLFactory.cs similarity index 89% rename from ImGui.App/ImGuiController/IOpenGLFactory.cs rename to ImGuiApp/ImGuiController/IOpenGLFactory.cs index 97235ef..a64957d 100644 --- a/ImGui.App/ImGuiController/IOpenGLFactory.cs +++ b/ImGuiApp/ImGuiController/IOpenGLFactory.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp; using Silk.NET.OpenGL; diff --git a/ImGui.App/ImGuiController/IOpenGLProvider.cs b/ImGuiApp/ImGuiController/IOpenGLProvider.cs similarity index 90% rename from ImGui.App/ImGuiController/IOpenGLProvider.cs rename to ImGuiApp/ImGuiController/IOpenGLProvider.cs index 839cfe6..22ca446 100644 --- a/ImGui.App/ImGuiController/IOpenGLProvider.cs +++ b/ImGuiApp/ImGuiController/IOpenGLProvider.cs @@ -2,8 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; - +namespace ktsu.ImGuiApp; using Silk.NET.OpenGL; /// diff --git a/ImGui.App/ImGuiController/ImGuiController.cs b/ImGuiApp/ImGuiController/ImGuiController.cs similarity index 95% rename from ImGui.App/ImGuiController/ImGuiController.cs rename to ImGuiApp/ImGuiController/ImGuiController.cs index 5b3e013..30f7055 100644 --- a/ImGui.App/ImGuiController/ImGuiController.cs +++ b/ImGuiApp/ImGuiController/ImGuiController.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using System; using System.Collections.Generic; @@ -15,9 +15,7 @@ namespace ktsu.ImGui.App.ImGuiController; using Silk.NET.OpenGL; using Silk.NET.Windowing; -using ktsu.ImGui.App; - -internal sealed class ImGuiController : IDisposable +internal class ImGuiController : IDisposable { internal GL? _gl; @@ -47,6 +45,8 @@ internal sealed class ImGuiController : IDisposable internal bool _disposed; + internal ImGuiContextPtr imGuiContextPtr; + /// /// Constructs a new ImGuiController. /// @@ -104,8 +104,8 @@ public ImGuiController(GL gl, IView view, IInputContext input, ImGuiFontConfig? SetPerFrameImGuiData(1f / 60f); - DebugLogger.Log("ImGuiController: Beginning frame"); - BeginFrame(); + DebugLogger.Log("ImGuiController: Setting up input handlers"); + SetupInputHandlers(); DebugLogger.Log("ImGuiController: Initialization completed"); } @@ -117,15 +117,24 @@ internal void Init(GL gl, IView view, IInputContext input) _windowWidth = view.Size.X; _windowHeight = view.Size.Y; - ImGui.CreateContext(); + imGuiContextPtr = ImGui.CreateContext(); + + // Initialize ImGui extensions (ImGuizmo, ImNodes, ImPlot) if available + ImGuiExtensionManager.Initialize(); + + // Create extension contexts and apply dark styles + ImGuiExtensionManager.CreateExtensionContexts(); + + SetupContext(); ImGui.StyleColorsDark(); } - internal void BeginFrame() + /// + /// Sets up input event handlers. Should only be called once during initialization. + /// + internal void SetupInputHandlers() { - ImGui.NewFrame(); - _frameBegun = true; _keyboard = _input?.Keyboards[0]; _mouse = _input?.Mice[0]; if (_view is not null) @@ -149,6 +158,16 @@ internal void BeginFrame() } } + /// + /// Sets up ImGui context for the current frame. Should be called every frame before Update(). + /// + internal void SetupContext() + { + // Set context for ImGui and extensions + ImGui.SetCurrentContext(imGuiContextPtr); + ImGuiExtensionManager.SetImGuiContext(imGuiContextPtr); + } + /// /// Delegate to receive keyboard key down events. /// @@ -267,8 +286,13 @@ internal void Update(float deltaSeconds) SetPerFrameImGuiData(deltaSeconds); UpdateImGuiInput(); - _frameBegun = true; + // Start ImGui frame first ImGui.NewFrame(); + _frameBegun = true; + + // IMPORTANT: Call extension BeginFrame methods AFTER ImGui.NewFrame() + // ImGuizmo, ImNodes, and ImPlot require ImGui to have started the frame first + ImGuiExtensionManager.BeginFrame(); } /// @@ -907,7 +931,7 @@ public void Dispose() /// Protected implementation of Dispose pattern. /// /// true if disposing managed resources, false if called from finalizer - private void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { if (_disposed) { @@ -935,6 +959,9 @@ private void Dispose(bool disposing) _fontTexture.Dispose(); _shader.Dispose(); + // Cleanup extension contexts before destroying ImGui context + ImGuiExtensionManager.Cleanup(); + ImGui.DestroyContext(); } } diff --git a/ImGui.App/ImGuiController/ImGuiFontConfig.cs b/ImGuiApp/ImGuiController/ImGuiFontConfig.cs similarity index 92% rename from ImGui.App/ImGuiController/ImGuiFontConfig.cs rename to ImGuiApp/ImGuiController/ImGuiFontConfig.cs index d694175..c9e89cb 100644 --- a/ImGui.App/ImGuiController/ImGuiFontConfig.cs +++ b/ImGuiApp/ImGuiController/ImGuiFontConfig.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using System; @@ -21,14 +21,9 @@ namespace ktsu.ImGui.App.ImGuiController; /// A function to get the glyph range for the font. /// Thrown when is less than or equal to zero. /// Thrown when is null. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1512:Use ArgumentOutOfRangeException throw helper", Justification = "")] public ImGuiFontConfig(string fontPath, int fontSize, Func? getGlyphRange = null) { - if (fontSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(fontSize)); - } - + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(fontSize); FontPath = fontPath ?? throw new ArgumentNullException(nameof(fontPath)); FontSize = fontSize; GetGlyphRange = getGlyphRange; diff --git a/ImGui.App/ImGuiController/OpenGLProvider.cs b/ImGuiApp/ImGuiController/OpenGLProvider.cs similarity index 95% rename from ImGui.App/ImGuiController/OpenGLProvider.cs rename to ImGuiApp/ImGuiController/OpenGLProvider.cs index 622b0cd..64b0272 100644 --- a/ImGui.App/ImGuiController/OpenGLProvider.cs +++ b/ImGuiApp/ImGuiController/OpenGLProvider.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; /// /// Provides access to OpenGL functionality. diff --git a/ImGui.App/ImGuiController/Shader.cs b/ImGuiApp/ImGuiController/Shader.cs similarity index 96% rename from ImGui.App/ImGuiController/Shader.cs rename to ImGuiApp/ImGuiController/Shader.cs index 8bc5e91..a57fc4e 100644 --- a/ImGui.App/ImGuiController/Shader.cs +++ b/ImGuiApp/ImGuiController/Shader.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using System; using System.Collections.Generic; @@ -19,7 +19,7 @@ internal struct UniformFieldInfo public UniformType Type; } -internal sealed class Shader : IDisposable +internal class Shader : IDisposable { public uint Program { get; private set; } internal readonly Dictionary _uniformToLocation = []; @@ -51,7 +51,7 @@ public void Dispose() /// Protected implementation of Dispose pattern. /// /// true if disposing managed resources, false if called from finalizer - private void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { if (_disposed) { diff --git a/ImGui.App/ImGuiController/Texture.cs b/ImGuiApp/ImGuiController/Texture.cs similarity index 96% rename from ImGui.App/ImGuiController/Texture.cs rename to ImGuiApp/ImGuiController/Texture.cs index 7af3be1..82516d5 100644 --- a/ImGui.App/ImGuiController/Texture.cs +++ b/ImGuiApp/ImGuiController/Texture.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using System; @@ -34,7 +34,7 @@ public enum TextureCoordinate R = TextureParameterName.TextureWrapR } -internal sealed class Texture : IDisposable +internal class Texture : IDisposable { public const SizedInternalFormat Srgb8Alpha8 = (SizedInternalFormat)GLEnum.Srgb8Alpha8; public const SizedInternalFormat Rgb32F = (SizedInternalFormat)GLEnum.Rgb32f; @@ -43,6 +43,7 @@ internal sealed class Texture : IDisposable public static float? MaxAniso; internal readonly GL _gl; + public readonly string? Name; public readonly uint GlTexture; public readonly uint Width, Height; public readonly uint MipmapLevels; @@ -131,7 +132,7 @@ public void Dispose() /// Protected implementation of Dispose pattern. /// /// true if disposing managed resources, false if called from finalizer - private void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { if (_disposed) { diff --git a/ImGui.App/ImGuiController/Util.cs b/ImGuiApp/ImGuiController/Util.cs similarity index 92% rename from ImGui.App/ImGuiController/Util.cs rename to ImGuiApp/ImGuiController/Util.cs index b8bc1cc..cfa0180 100644 --- a/ImGui.App/ImGuiController/Util.cs +++ b/ImGuiApp/ImGuiController/Util.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp.ImGuiController; using System.Diagnostics; using System.Diagnostics.Contracts; diff --git a/ImGui.App/ImGuiController/WindowOpenGLFactory.cs b/ImGuiApp/ImGuiController/WindowOpenGLFactory.cs similarity index 93% rename from ImGui.App/ImGuiController/WindowOpenGLFactory.cs rename to ImGuiApp/ImGuiController/WindowOpenGLFactory.cs index 11f2ccb..f20ca34 100644 --- a/ImGui.App/ImGuiController/WindowOpenGLFactory.cs +++ b/ImGuiApp/ImGuiController/WindowOpenGLFactory.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App.ImGuiController; +namespace ktsu.ImGuiApp; using Silk.NET.OpenGL; using Silk.NET.Windowing; diff --git a/ImGuiApp/ImGuiExtensionManager.cs b/ImGuiApp/ImGuiExtensionManager.cs new file mode 100644 index 0000000..9ea78d4 --- /dev/null +++ b/ImGuiApp/ImGuiExtensionManager.cs @@ -0,0 +1,395 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp; + +using System.Reflection; +using Hexa.NET.ImGui; + +/// +/// Manages automatic initialization and lifecycle of ImGui extensions like ImGuizmo, ImNodes, and ImPlot. +/// +public static class ImGuiExtensionManager +{ + // Cached reflection info for performance + private static MethodInfo? imGuizmoBeginFrame; + private static MethodInfo? imGuizmoSetImGuiContext; + private static MethodInfo? imNodesBeginFrame; + private static MethodInfo? imNodesSetImGuiContext; + private static MethodInfo? imNodesCreateContext; + private static MethodInfo? imNodesSetCurrentContext; + private static MethodInfo? imNodesStyleColorsDark; + private static MethodInfo? imNodesGetStyle; + private static MethodInfo? imNodesDestroyContext; + private static MethodInfo? imPlotBeginFrame; + private static MethodInfo? imPlotSetImGuiContext; + private static MethodInfo? imPlotCreateContext; + private static MethodInfo? imPlotSetCurrentContext; + private static MethodInfo? imPlotStyleColorsDark; + private static MethodInfo? imPlotGetStyle; + private static MethodInfo? imPlotDestroyContext; + + // Extension contexts + private static object? nodesContext; + private static object? plotContext; + + private static bool initialized; + + /// + /// Initialize extension detection. Called once during application startup. + /// + public static void Initialize() + { + if (initialized) + { + return; + } + + initialized = true; + + InitializeImGuizmo(); + InitializeImNodes(); + InitializeImPlot(); + } + + private static void InitializeImGuizmo() + { + try + { + Assembly imGuizmoAssembly = Assembly.Load("Hexa.NET.ImGuizmo"); + Type? imGuizmoType = imGuizmoAssembly.GetType("Hexa.NET.ImGuizmo.ImGuizmo"); + imGuizmoBeginFrame = imGuizmoType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + + // Find SetImGuiContext method that takes ImGuiContextPtr parameter + Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr"); + if (imGuiContextPtrType != null) + { + imGuizmoSetImGuiContext = imGuizmoType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]); + } + + if (imGuizmoBeginFrame != null) + { + DebugLogger.Log("ImGuiExtensionManager: ImGuizmo detected and will be auto-initialized"); + } + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException) + { + // ImGuizmo not available or has ambiguous methods - this is fine + DebugLogger.Log($"ImGuiExtensionManager: ImGuizmo not available or has issues: {ex.Message}"); + } + } + + private static void InitializeImNodes() + { + try + { + Assembly imNodesAssembly = Assembly.Load("Hexa.NET.ImNodes"); + Type? imNodesType = imNodesAssembly.GetType("Hexa.NET.ImNodes.ImNodes"); + imNodesBeginFrame = imNodesType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + imNodesCreateContext = imNodesType?.GetMethod("CreateContext", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + imNodesGetStyle = imNodesType?.GetMethod("GetStyle", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + + // Find SetImGuiContext method that takes ImGuiContextPtr parameter + Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr"); + if (imGuiContextPtrType != null) + { + imNodesSetImGuiContext = imNodesType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]); + } + + // Find context-related methods + if (imNodesCreateContext != null) + { + Type? contextType = imNodesCreateContext.ReturnType; + if (contextType != null) + { + imNodesSetCurrentContext = imNodesType?.GetMethod("SetCurrentContext", BindingFlags.Public | BindingFlags.Static, [contextType]); + + // Find DestroyContext method that takes the context type parameter + imNodesDestroyContext = imNodesType?.GetMethod("DestroyContext", BindingFlags.Public | BindingFlags.Static, [contextType]); + + // Find StyleColorsDark method that takes ImNodesStylePtr parameter + Type? styleType = imNodesAssembly.GetType("Hexa.NET.ImNodes.ImNodesStylePtr"); + if (styleType != null) + { + imNodesStyleColorsDark = imNodesType?.GetMethod("StyleColorsDark", BindingFlags.Public | BindingFlags.Static, [styleType]); + } + } + } + + if (imNodesBeginFrame != null) + { + DebugLogger.Log("ImGuiExtensionManager: ImNodes detected and will be auto-initialized"); + } + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException) + { + // ImNodes not available or has ambiguous methods - this is fine + DebugLogger.Log($"ImGuiExtensionManager: ImNodes not available or has issues: {ex.Message}"); + } + } + + private static void InitializeImPlot() + { + try + { + Assembly imPlotAssembly = Assembly.Load("Hexa.NET.ImPlot"); + Type? imPlotType = imPlotAssembly.GetType("Hexa.NET.ImPlot.ImPlot"); + imPlotBeginFrame = imPlotType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + imPlotCreateContext = imPlotType?.GetMethod("CreateContext", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + imPlotGetStyle = imPlotType?.GetMethod("GetStyle", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes); + + // Find SetImGuiContext method that takes ImGuiContextPtr parameter + Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr"); + if (imGuiContextPtrType != null) + { + imPlotSetImGuiContext = imPlotType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]); + } + + // Find context-related methods + if (imPlotCreateContext != null) + { + Type? contextType = imPlotCreateContext.ReturnType; + if (contextType != null) + { + imPlotSetCurrentContext = imPlotType?.GetMethod("SetCurrentContext", BindingFlags.Public | BindingFlags.Static, [contextType]); + + // Find DestroyContext method that takes the context type parameter + imPlotDestroyContext = imPlotType?.GetMethod("DestroyContext", BindingFlags.Public | BindingFlags.Static, [contextType]); + + // Find StyleColorsDark method that takes ImPlotStylePtr parameter + Type? styleType = imPlotAssembly.GetType("Hexa.NET.ImPlot.ImPlotStylePtr"); + if (styleType != null) + { + imPlotStyleColorsDark = imPlotType?.GetMethod("StyleColorsDark", BindingFlags.Public | BindingFlags.Static, [styleType]); + } + } + } + + if (imPlotBeginFrame != null) + { + DebugLogger.Log("ImGuiExtensionManager: ImPlot detected and will be auto-initialized"); + } + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException) + { + // ImPlot not available or has ambiguous methods - this is fine + DebugLogger.Log($"ImGuiExtensionManager: ImPlot not available or has issues: {ex.Message}"); + } + } + + /// + /// Call BeginFrame for all detected extensions. Should be called once per frame after ImGui.NewFrame(). + /// + public static void BeginFrame() + { + // Call ImGuizmo.BeginFrame() if available + try + { + imGuizmoBeginFrame?.Invoke(null, null); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImGuizmo.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}"); + } + + // Call ImNodes.BeginFrame() if available + try + { + imNodesBeginFrame?.Invoke(null, null); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImNodes.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}"); + } + + // Call ImPlot.BeginFrame() if available + try + { + imPlotBeginFrame?.Invoke(null, null); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImPlot.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}"); + } + } + + /// + /// Sets the ImGui context for all detected extensions. Should be called once after ImGui.CreateContext(). + /// + /// The ImGui context to set for the extensions. If null, uses the current context. + public static void SetImGuiContext(ImGuiContextPtr? context = null) + { + // Use current context if none provided + ImGuiContextPtr contextToSet = context ?? ImGui.GetCurrentContext(); + + // Set context for ImGuizmo if available + try + { + imGuizmoSetImGuiContext?.Invoke(null, [contextToSet]); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImGuizmo.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}"); + } + + // Set context for ImNodes if available + try + { + imNodesSetImGuiContext?.Invoke(null, [contextToSet]); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImNodes.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}"); + } + + // Set context for ImPlot if available + try + { + imPlotSetImGuiContext?.Invoke(null, [contextToSet]); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error calling ImPlot.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}"); + } + } + + /// + /// Creates extension contexts and applies dark styles. Should be called once after SetImGuiContext(). + /// + public static void CreateExtensionContexts() + { + // Create and set ImNodes context and set style + if (imNodesCreateContext != null && imNodesSetCurrentContext != null) + { + try + { + nodesContext = imNodesCreateContext.Invoke(null, null); + if (nodesContext != null) + { + imNodesSetCurrentContext.Invoke(null, [nodesContext]); + + // Apply dark style if available + if (imNodesStyleColorsDark != null && imNodesGetStyle != null) + { + object? style = imNodesGetStyle.Invoke(null, null); + if (style != null) + { + imNodesStyleColorsDark.Invoke(null, [style]); + } + } + + DebugLogger.Log("ImGuiExtensionManager: ImNodes context created and dark style applied"); + } + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error creating ImNodes context: {ex.InnerException?.Message ?? ex.Message}"); + } + } + + // Create and set ImPlot context and set style + if (imPlotCreateContext != null && imPlotSetCurrentContext != null) + { + try + { + plotContext = imPlotCreateContext.Invoke(null, null); + if (plotContext != null) + { + imPlotSetCurrentContext.Invoke(null, [plotContext]); + + // Apply dark style if available + if (imPlotStyleColorsDark != null && imPlotGetStyle != null) + { + object? style = imPlotGetStyle.Invoke(null, null); + if (style != null) + { + imPlotStyleColorsDark.Invoke(null, [style]); + } + } + + DebugLogger.Log("ImGuiExtensionManager: ImPlot context created and dark style applied"); + } + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error creating ImPlot context: {ex.InnerException?.Message ?? ex.Message}"); + } + } + } + + /// + /// Cleanup extension contexts. Should be called during application shutdown. + /// + public static void Cleanup() + { + // Clear ImGuizmo context by setting it to null to free any internal state + if (imGuizmoSetImGuiContext != null) + { + try + { + imGuizmoSetImGuiContext.Invoke(null, [default(ImGuiContextPtr)]); + DebugLogger.Log("ImGuiExtensionManager: ImGuizmo context cleared"); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error clearing ImGuizmo context: {ex.InnerException?.Message ?? ex.Message}"); + } + } + + // Destroy ImNodes context if created + if (nodesContext != null && imNodesDestroyContext != null) + { + try + { + imNodesDestroyContext.Invoke(null, [nodesContext]); + nodesContext = null; + DebugLogger.Log("ImGuiExtensionManager: ImNodes context destroyed"); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error destroying ImNodes context: {ex.InnerException?.Message ?? ex.Message}"); + } + } + + // Destroy ImPlot context if created + if (plotContext != null && imPlotDestroyContext != null) + { + try + { + imPlotDestroyContext.Invoke(null, [plotContext]); + plotContext = null; + DebugLogger.Log("ImGuiExtensionManager: ImPlot context destroyed"); + } + catch (TargetInvocationException ex) + { + DebugLogger.Log($"ImGuiExtensionManager: Error destroying ImPlot context: {ex.InnerException?.Message ?? ex.Message}"); + } + } + } + + /// + /// Gets whether ImGuizmo is available and initialized. + /// + public static bool IsImGuizmoAvailable => imGuizmoBeginFrame != null; + + /// + /// Gets whether ImNodes is available and initialized. + /// + public static bool IsImNodesAvailable => imNodesBeginFrame != null; + + /// + /// Gets whether ImPlot is available and initialized. + /// + public static bool IsImPlotAvailable => imPlotBeginFrame != null; + + /// + /// Gets whether ImNodes context has been created. + /// + public static bool IsImNodesContextCreated => nodesContext != null; + + /// + /// Gets whether ImPlot context has been created. + /// + public static bool IsImPlotContextCreated => plotContext != null; +} diff --git a/ImGui.App/NativeMethods.cs b/ImGuiApp/NativeMethods.cs similarity index 61% rename from ImGui.App/NativeMethods.cs rename to ImGuiApp/NativeMethods.cs index 6d1ec77..0a93c4d 100644 --- a/ImGui.App/NativeMethods.cs +++ b/ImGuiApp/NativeMethods.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Runtime.InteropServices; internal static partial class NativeMethods @@ -21,56 +21,9 @@ internal static partial class NativeMethods /// /// True if the operation was successful; otherwise, false. [LibraryImport("user32.dll")] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool SetProcessDPIAware(); - /// - /// Sets the DPI awareness context for the process (Windows 10 version 1607 and later). - /// - /// The DPI awareness context to set. - /// The previous DPI awareness context, or null if the function failed. - [LibraryImport("user32.dll")] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - internal static partial nint SetProcessDpiAwarenessContext(nint dpiContext); - - /// - /// Sets the DPI awareness for the process (Windows 8.1 and later). - /// - /// The DPI awareness value to set. - /// HRESULT indicating success or failure. - [LibraryImport("Shcore.dll")] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] - internal static partial int SetProcessDpiAwareness(ProcessDpiAwareness value); - - /// - /// DPI awareness values for SetProcessDpiAwareness. - /// - internal enum ProcessDpiAwareness - { - /// - /// DPI unaware. This app does not scale for DPI changes and is always assumed to have a scale factor of 100% (96 DPI). - /// - ProcessDpiUnaware = 0, - - /// - /// System DPI aware. This app does not scale for DPI changes. - /// - ProcessSystemDpiAware = 1, - - /// - /// Per monitor DPI aware. This app checks for the DPI when it is created and adjusts the scale factor whenever the DPI changes. - /// - ProcessPerMonitorDpiAware = 2 - } - - // DPI Awareness Context constants - internal static readonly nint DPI_AWARENESS_CONTEXT_UNAWARE = new(-1); - internal static readonly nint DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = new(-2); - internal static readonly nint DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = new(-3); - internal static readonly nint DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = new(-4); - internal static readonly nint DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = new(-5); - internal const string X11LibraryName = "libX11.so.6"; /// @@ -79,7 +32,6 @@ internal enum ProcessDpiAwareness /// The name of the display. /// A handle to the display structure. [LibraryImport(X11LibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial IntPtr XOpenDisplay([MarshalAs(UnmanagedType.LPStr)] string display); /// @@ -90,7 +42,6 @@ internal enum ProcessDpiAwareness /// The option name. /// The value of the default setting. [LibraryImport(X11LibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial IntPtr XGetDefault(IntPtr display, [MarshalAs(UnmanagedType.LPStr)] string program, [MarshalAs(UnmanagedType.LPStr)] string option); /// @@ -100,7 +51,6 @@ internal enum ProcessDpiAwareness /// The screen number. /// The width of the screen in pixels. [LibraryImport(X11LibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int XDisplayWidth(IntPtr display, int screenNumber); /// @@ -110,7 +60,6 @@ internal enum ProcessDpiAwareness /// The screen number. /// The width of the screen in millimeters. [LibraryImport(X11LibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int XDisplayWidthMM(IntPtr display, int screenNumber); /// @@ -119,7 +68,6 @@ internal enum ProcessDpiAwareness /// A handle to the display structure. /// Zero if the operation was successful; otherwise, a non-zero value. [LibraryImport(X11LibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int XCloseDisplay(IntPtr display); /// @@ -160,18 +108,14 @@ internal struct StartupOutput private const string GDILibraryName = "gdiplus.dll"; [LibraryImport(GDILibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int GdiplusStartup(out IntPtr token, in StartupInputEx input, out StartupOutput output); [LibraryImport(GDILibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int GdipCreateFromHWND(IntPtr hwnd, out IntPtr graphics); [LibraryImport(GDILibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int GdipDeleteGraphics(IntPtr graphics); [LibraryImport(GDILibraryName)] - [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] internal static partial int GdipGetDpiX(IntPtr graphics, out float dpi); } diff --git a/ImGui.App/PidFrameLimiter.cs b/ImGuiApp/PidFrameLimiter.cs similarity index 97% rename from ImGui.App/PidFrameLimiter.cs rename to ImGuiApp/PidFrameLimiter.cs index 1adb85d..be14c77 100644 --- a/ImGui.App/PidFrameLimiter.cs +++ b/ImGuiApp/PidFrameLimiter.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Diagnostics; using System.Threading; @@ -11,7 +11,7 @@ namespace ktsu.ImGui.App; /// A PID controller-based frame limiter that provides accurate frame rate control /// by learning from past errors and dynamically adjusting sleep times. /// -internal sealed class PidFrameLimiter +internal class PidFrameLimiter { internal readonly double kp; // Proportional gain internal readonly double ki; // Integral gain @@ -538,17 +538,6 @@ public void LimitFrameRate(double targetFrameTimeMs) { HandleAutoTuningProgression(error); } - - // Optional: Log PID state for debugging (remove in production) -#if DEBUG - if (DateTime.UtcNow.Millisecond % 100 < 16) // Log roughly every 100ms - { - Debug.WriteLine( - $"PID Frame Limiter - Target: {targetFrameTimeMs:F1}ms, " + - $"Actual: {smoothedFrameTime:F1}ms, Error: {error:F1}ms, " + - $"Sleep: {baseSleepMs:F1}ms, P: {CurrentKp * error:F2}, I: {CurrentKi * integral:F2}, D: {CurrentKd * derivative:F2}"); - } -#endif } internal static double CalculateStability(List errors) diff --git a/ImGui.App/Resources/NerdFont.ttf b/ImGuiApp/Resources/NerdFont.ttf similarity index 100% rename from ImGui.App/Resources/NerdFont.ttf rename to ImGuiApp/Resources/NerdFont.ttf diff --git a/ImGui.App/Resources/NotoEmoji.ttf b/ImGuiApp/Resources/NotoEmoji.ttf similarity index 100% rename from ImGui.App/Resources/NotoEmoji.ttf rename to ImGuiApp/Resources/NotoEmoji.ttf diff --git a/ImGui.App/Resources/Resources.Designer.cs b/ImGuiApp/Resources/Resources.Designer.cs similarity index 95% rename from ImGui.App/Resources/Resources.Designer.cs rename to ImGuiApp/Resources/Resources.Designer.cs index 37e8317..a78a23a 100644 --- a/ImGui.App/Resources/Resources.Designer.cs +++ b/ImGuiApp/Resources/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace ktsu.ImGui.App.Resources +namespace ktsu.ImGuiApp.Resources { using System; @@ -45,7 +45,7 @@ internal Resources() { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ktsu.ImGui.App.Resources.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ktsu.ImGuiApp.Resources.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/ImGui.App/Resources/Resources.resx b/ImGuiApp/Resources/Resources.resx similarity index 100% rename from ImGui.App/Resources/Resources.resx rename to ImGuiApp/Resources/Resources.resx diff --git a/ImGui.App/UIScaler.cs b/ImGuiApp/UIScaler.cs similarity index 99% rename from ImGui.App/UIScaler.cs rename to ImGuiApp/UIScaler.cs index 313b634..f559418 100644 --- a/ImGui.App/UIScaler.cs +++ b/ImGuiApp/UIScaler.cs @@ -2,7 +2,7 @@ // All rights reserved. // Licensed under the MIT license. -namespace ktsu.ImGui.App; +namespace ktsu.ImGuiApp; using System.Numerics; diff --git a/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs b/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs new file mode 100644 index 0000000..b93681e --- /dev/null +++ b/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs @@ -0,0 +1,89 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; + +/// +/// Demo for advanced ImGui widgets +/// +internal sealed class AdvancedWidgetsDemo : IDemoTab +{ + private Vector3 colorPickerValue = new(0.4f, 0.7f, 0.2f); + private Vector4 color4Value = new(1.0f, 0.5f, 0.2f, 1.0f); + private float animationTime; + + public string TabName => "Advanced Widgets"; + + public void Update(float deltaTime) => animationTime += deltaTime; + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + // Color controls + ImGui.SeparatorText("Color Controls:"); + ImGui.ColorEdit3("Color RGB", ref colorPickerValue); + ImGui.ColorEdit4("Color RGBA", ref color4Value); + ImGui.SetNextItemWidth(200.0f); + ImGui.ColorPicker3("Color Picker", ref colorPickerValue); + + // Tree view + ImGui.SeparatorText("Tree View:"); + if (ImGui.TreeNode("Root Node")) + { + for (int i = 0; i < 5; i++) + { + string nodeName = $"Child Node {i}"; + bool nodeOpen = ImGui.TreeNode(nodeName); + + if (i == 2 && nodeOpen) + { + for (int j = 0; j < 3; j++) + { + if (ImGui.TreeNode($"Grandchild {j}")) + { + ImGui.Text($"Leaf item {j}"); + ImGui.TreePop(); + } + } + } + else if (nodeOpen) + { + ImGui.Text($"Content of {nodeName}"); + } + + if (nodeOpen) + { + ImGui.TreePop(); + } + } + ImGui.TreePop(); + } + + // Progress bars and loading indicators + ImGui.SeparatorText("Progress Indicators:"); + float progress = ((float)Math.Sin(animationTime * 2.0) * 0.5f) + 0.5f; + ImGui.ProgressBar(progress, new Vector2(-1, 0), $"{progress * 100:F1}%"); + + // Spinner-like effect + ImGui.Text("Loading..."); + ImGui.SameLine(); + for (int i = 0; i < 8; i++) + { + float rotation = (animationTime * 5.0f) + (i * MathF.PI / 4.0f); + float alpha = (MathF.Sin(rotation) + 1.0f) * 0.5f; + ImGui.TextColored(new Vector4(1, 1, 1, alpha), "●"); + if (i < 7) + { + ImGui.SameLine(); + } + } + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/AnimationDemo.cs b/ImGuiAppDemo/Demos/AnimationDemo.cs new file mode 100644 index 0000000..fcf930d --- /dev/null +++ b/ImGuiAppDemo/Demos/AnimationDemo.cs @@ -0,0 +1,71 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; + +/// +/// Demo for animations and effects +/// +internal sealed class AnimationDemo : IDemoTab +{ + private float animationTime; + private float bounceOffset; + private float pulseScale = 1.0f; + private float textSpeed = 50.0f; + + public string TabName => "Animation & Effects"; + + public void Update(float deltaTime) + { + animationTime += deltaTime; + + // Bouncing animation + bounceOffset = MathF.Abs(MathF.Sin(animationTime * 3)) * 50; + + // Pulse animation + pulseScale = 0.8f + (0.4f * MathF.Sin(animationTime * 4)); + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.SeparatorText("Animation Examples:"); + + // Simple animations + ImGui.Text("Bouncing Animation:"); + Vector2 ballPos = ImGui.GetCursorScreenPos(); + ballPos.Y += bounceOffset; + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.AddCircleFilled(ballPos + new Vector2(50, 50), 20, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 0, 1))); + ImGui.Dummy(new Vector2(100, 100)); + + // Pulsing element + ImGui.Text("Pulse Animation:"); + Vector2 pulsePos = ImGui.GetCursorScreenPos(); + float pulseSize = 20 * pulseScale; + drawList.AddCircleFilled(pulsePos + new Vector2(50, 50), pulseSize, + ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0, 1, 0.7f))); + ImGui.Dummy(new Vector2(100, 100)); + + ImGui.SeparatorText("Animated Text:"); + ImGui.SliderFloat("Text Speed", ref textSpeed, 10.0f, 200.0f); + + for (int i = 0; i < 20; i++) + { + float wave = (MathF.Sin((animationTime * 3.0f) + (i * 0.5f)) * 0.5f) + 0.5f; + ImGui.TextColored(new Vector4(wave, 1.0f - wave, 0.5f, 1.0f), i % 5 == 4 ? " " : "▓"); + if (i % 5 != 4) + { + ImGui.SameLine(); + } + } + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs b/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs new file mode 100644 index 0000000..4f8d96b --- /dev/null +++ b/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs @@ -0,0 +1,100 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; + +/// +/// Demo for basic ImGui widgets +/// +internal sealed class BasicWidgetsDemo : IDemoTab +{ + private float sliderValue = 0.5f; + private int counter; + private bool checkboxState; + private string inputText = "Type here..."; + private int comboSelection; + private readonly string[] comboItems = ["Item 1", "Item 2", "Item 3", "Item 4"]; + private int listboxSelection; + private readonly string[] listboxItems = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]; + private float dragFloat = 1.0f; + private int dragInt = 50; + private Vector3 dragVector = new(1.0f, 2.0f, 3.0f); + private float angle; + private int radioSelection; + + public string TabName => "Basic Widgets"; + + public void Update(float deltaTime) + { + // No updates needed for basic widgets + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.TextWrapped("This tab demonstrates basic ImGui widgets and controls."); + + // Buttons + ImGui.SeparatorText("Buttons:"); + if (ImGui.Button("Regular Button")) + { + counter++; + } + + ImGui.SameLine(); + if (ImGui.SmallButton("Small")) + { + counter++; + } + + ImGui.SameLine(); + if (ImGui.ArrowButton("##left", ImGuiDir.Left)) + { + counter--; + } + + ImGui.SameLine(); + if (ImGui.ArrowButton("##right", ImGuiDir.Right)) + { + counter++; + } + + ImGui.SameLine(); + ImGui.Text($"Counter: {counter}"); + + // Checkboxes and Radio buttons + ImGui.SeparatorText("Selection Controls"); + ImGui.Checkbox("Checkbox", ref checkboxState); + + ImGui.RadioButton("Option 1", ref radioSelection, 0); + ImGui.SameLine(); + ImGui.RadioButton("Option 2", ref radioSelection, 1); + ImGui.SameLine(); + ImGui.RadioButton("Option 3", ref radioSelection, 2); + + // Sliders + ImGui.SeparatorText("Sliders"); + ImGui.SliderFloat("Float Slider", ref sliderValue, 0.0f, 1.0f); + ImGui.SliderFloat("Angle", ref angle, 0.0f, 360.0f, "%.1f deg"); + ImGui.SliderInt("Int Slider", ref dragInt, 0, 100); + + // Input fields + ImGui.SeparatorText("Input Fields"); + ImGui.InputText("Text Input", ref inputText, 100); + ImGui.InputFloat("Float Input", ref dragFloat); + ImGui.InputFloat3("Vector3 Input", ref dragVector); + + // Combo boxes + ImGui.SeparatorText("Dropdowns"); + ImGui.Combo("Combo Box", ref comboSelection, comboItems, comboItems.Length); + ImGui.ListBox("List Box", ref listboxSelection, listboxItems, listboxItems.Length, 4); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/DataVisualizationDemo.cs b/ImGuiAppDemo/Demos/DataVisualizationDemo.cs new file mode 100644 index 0000000..8a07ae3 --- /dev/null +++ b/ImGuiAppDemo/Demos/DataVisualizationDemo.cs @@ -0,0 +1,87 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; +using ktsu.ImGuiApp; +using ktsu.ImGuiAppDemo.Properties; + +/// +/// Demo for data visualization features +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")] +internal sealed class DataVisualizationDemo : IDemoTab +{ + private readonly List plotValues = []; + private readonly Random random = new(); + private float plotRefreshTime; + + public string TabName => "Data Visualization"; + + public void Update(float deltaTime) + { + // Update plot data + plotRefreshTime += deltaTime; + if (plotRefreshTime >= 0.1f) // Update every 100ms + { + plotRefreshTime = 0; + plotValues.Add((float)random.NextDouble()); + if (plotValues.Count > 100) // Keep last 100 values + { + plotValues.RemoveAt(0); + } + } + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.SeparatorText("Real-time Data Plots:"); + + // Line plot + if (plotValues.Count > 0) + { + float[] values = [.. plotValues]; + ImGui.PlotLines("Random Values", ref values[0], values.Length, 0, + $"Current: {values[^1]:F2}", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100)); + + ImGui.PlotHistogram("Distribution", ref values[0], values.Length, 0, + "Histogram", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100)); + } + + // Performance note + ImGui.SeparatorText("Performance Metrics:"); + ImGui.TextWrapped("Performance monitoring is now available in the Debug menu! Use 'Debug > Show Performance Monitor' to see real-time FPS graphs and throttling state."); + + // Font demonstrations + ImGui.SeparatorText("Custom Font Rendering:"); + using (new FontAppearance(nameof(Resources.CARDCHAR), 16)) + { + ImGui.Text("Small custom font text"); + } + + using (new FontAppearance(nameof(Resources.CARDCHAR), 24)) + { + ImGui.Text("Medium custom font text"); + } + + using (new FontAppearance(nameof(Resources.CARDCHAR), 32)) + { + ImGui.Text("Large custom font text"); + } + + // Text formatting examples + ImGui.SeparatorText("Text Formatting:"); + ImGui.TextColored(new Vector4(1, 0, 0, 1), "Red text"); + ImGui.TextColored(new Vector4(0, 1, 0, 1), "Green text"); + ImGui.TextColored(new Vector4(0, 0, 1, 1), "Blue text"); + ImGui.TextWrapped("This is a long line of text that should wrap to multiple lines when the window is not wide enough to contain it all on a single line."); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/GraphicsDemo.cs b/ImGuiAppDemo/Demos/GraphicsDemo.cs new file mode 100644 index 0000000..a356a3e --- /dev/null +++ b/ImGuiAppDemo/Demos/GraphicsDemo.cs @@ -0,0 +1,97 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; +using ktsu.Extensions; +using ktsu.ImGuiApp; +using ktsu.Semantics; + +/// +/// Demo for graphics and drawing capabilities +/// +internal sealed class GraphicsDemo : IDemoTab +{ + private readonly List canvasPoints = []; + private Vector4 drawColor = new(1.0f, 1.0f, 0.0f, 1.0f); + private float brushSize = 5.0f; + private float animationTime; + + public string TabName => "Graphics & Drawing"; + + public void Update(float deltaTime) => animationTime += deltaTime; + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + // Image display + AbsoluteFilePath iconPath = AppContext.BaseDirectory.As() / "icon.png".As(); + ImGuiAppTextureInfo iconTexture = ImGuiApp.GetOrLoadTexture(iconPath); + + ImGui.SeparatorText("Image Display:"); + ImGui.Image(iconTexture.TextureRef, new Vector2(64, 64)); + ImGui.SameLine(); + ImGui.Image(iconTexture.TextureRef, new Vector2(32, 32)); + ImGui.SameLine(); + ImGui.Image(iconTexture.TextureRef, new Vector2(16, 16)); + + // Custom drawing with ImDrawList + ImGui.SeparatorText("Custom Drawing Canvas:"); + ImGui.ColorEdit4("Draw Color", ref drawColor); + ImGui.SliderFloat("Brush Size", ref brushSize, 1.0f, 20.0f); + + if (ImGui.Button("Clear Canvas")) + { + canvasPoints.Clear(); + } + + Vector2 canvasPos = ImGui.GetCursorScreenPos(); + Vector2 canvasSize = new(400, 200); + + // Draw canvas background + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + drawList.AddRectFilled(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.1f, 0.1f, 0.1f, 1.0f))); + drawList.AddRect(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0.5f, 0.5f, 1.0f))); + + // Handle mouse input for drawing + ImGui.InvisibleButton("Canvas", canvasSize); + if (ImGui.IsItemActive() && ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + Vector2 mousePos = ImGui.GetMousePos() - canvasPos; + if (mousePos.X >= 0 && mousePos.Y >= 0 && mousePos.X <= canvasSize.X && mousePos.Y <= canvasSize.Y) + { + canvasPoints.Add(mousePos); + } + } + + // Draw points + uint color = ImGui.ColorConvertFloat4ToU32(drawColor); + foreach (Vector2 point in canvasPoints) + { + drawList.AddCircleFilled(canvasPos + point, brushSize, color); + } + + // Draw some simple shapes for demonstration + ImGui.SeparatorText("Shape Examples:"); + Vector2 shapeStart = ImGui.GetCursorScreenPos(); + + // Simple animated circle + float t = animationTime; + Vector2 center = shapeStart + new Vector2(100, 50); + float radius = 20 + (MathF.Sin(t * 2) * 5); + drawList.AddCircle(center, radius, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 1)), 16, 2.0f); + + // Moving rectangle + Vector2 rectPos = shapeStart + new Vector2(200 + (MathF.Sin(t) * 30), 30); + drawList.AddRectFilled(rectPos, rectPos + new Vector2(40, 40), ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 0, 0.7f))); + + ImGui.Dummy(new Vector2(400, 100)); // Reserve space + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/IDemoTab.cs b/ImGuiAppDemo/Demos/IDemoTab.cs new file mode 100644 index 0000000..9b80597 --- /dev/null +++ b/ImGuiAppDemo/Demos/IDemoTab.cs @@ -0,0 +1,27 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +/// +/// Interface for demo tab implementations +/// +internal interface IDemoTab +{ + /// + /// Gets the name of the tab to display in the UI + /// + public string TabName { get; } + + /// + /// Renders the demo tab content + /// + public void Render(); + + /// + /// Updates the demo state (called each frame) + /// + /// Time elapsed since last frame + public void Update(float deltaTime); +} diff --git a/ImGuiAppDemo/Demos/ImGuizmoDemo.cs b/ImGuiAppDemo/Demos/ImGuizmoDemo.cs new file mode 100644 index 0000000..9a02d7b --- /dev/null +++ b/ImGuiAppDemo/Demos/ImGuizmoDemo.cs @@ -0,0 +1,127 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System; +using System.Numerics; +using Hexa.NET.ImGui; +using Hexa.NET.ImGuizmo; + +/// +/// Demo for ImGuizmo 3D manipulation gizmos +/// +internal sealed class ImGuizmoDemo : IDemoTab +{ + private Matrix4x4 gizmoTransform = Matrix4x4.Identity; + private Matrix4x4 gizmoView = Matrix4x4.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.UnitY); + private Matrix4x4 gizmoProjection; + private ImGuizmoOperation gizmoOperation = ImGuizmoOperation.Translate; + private ImGuizmoMode gizmoMode = ImGuizmoMode.Local; + private bool gizmoEnabled = true; + private float animationTime; + + public string TabName => "ImGuizmo 3D Gizmos"; + + public void Update(float deltaTime) + { + animationTime += deltaTime; + + // Update gizmo view matrix for rotation demo + float cameraAngle = animationTime * 0.2f; + Vector3 cameraPos = new(MathF.Sin(cameraAngle) * 5f, 3f, MathF.Cos(cameraAngle) * 5f); + gizmoView = Matrix4x4.CreateLookAt(cameraPos, Vector3.Zero, Vector3.UnitY); + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.TextWrapped("ImGuizmo provides 3D manipulation gizmos for translate, rotate, and scale operations."); + + // Gizmo controls + ImGui.SeparatorText("Gizmo Controls:."); + ImGui.Checkbox("Enable Gizmo", ref gizmoEnabled); + + // Operation selection + string[] operationNames = Enum.GetNames(); + ImGuizmoOperation[] operations = Enum.GetValues(); + int opIndex = Array.IndexOf(operations, gizmoOperation); + if (ImGui.Combo("Operation", ref opIndex, operationNames, operationNames.Length)) + { + gizmoOperation = operations[opIndex]; + } + + // Mode selection + string[] modeNames = Enum.GetNames(); + ImGuizmoMode[] modes = Enum.GetValues(); + int modeIndex = Array.IndexOf(modes, gizmoMode); + if (ImGui.Combo("Mode", ref modeIndex, modeNames, modeNames.Length)) + { + gizmoMode = modes[modeIndex]; + } + + // Display transform matrix values + ImGui.SeparatorText("Transform Matrix:"); + ImGui.Text($"[{gizmoTransform.M11:F2}, {gizmoTransform.M12:F2}, {gizmoTransform.M13:F2}, {gizmoTransform.M14:F2}]"); + ImGui.Text($"[{gizmoTransform.M21:F2}, {gizmoTransform.M22:F2}, {gizmoTransform.M23:F2}, {gizmoTransform.M24:F2}]"); + ImGui.Text($"[{gizmoTransform.M31:F2}, {gizmoTransform.M32:F2}, {gizmoTransform.M33:F2}, {gizmoTransform.M34:F2}]"); + ImGui.Text($"[{gizmoTransform.M41:F2}, {gizmoTransform.M42:F2}, {gizmoTransform.M43:F2}, {gizmoTransform.M44:F2}]"); + + if (ImGui.Button("Reset Transform")) + { + gizmoTransform = Matrix4x4.Identity; + } + + // Gizmo viewport - use a reasonable size instead of all available space + Vector2 availableSize = ImGui.GetContentRegionAvail(); + Vector2 gizmoSize = new(availableSize.X, Math.Min(availableSize.Y, availableSize.X * 0.6f)); // Use width-based aspect ratio + + // Get position for gizmo (NO space reservation - see what happens!) + Vector2 gizmoPos = ImGui.GetCursorScreenPos(); + + // Update projection matrix based on gizmo size + float aspectRatio = gizmoSize.Y > 0 ? gizmoSize.X / gizmoSize.Y : 1.0f; + gizmoProjection = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 4f, aspectRatio, 0.1f, 100f); + + // Set up ImGuizmo for this viewport + if (gizmoEnabled) + { + // IMPORTANT: Set the drawlist and enable ImGuizmo and set the rect, before using any ImGuizmo functions + ImGuizmo.SetDrawlist(); + ImGuizmo.Enable(true); + ImGuizmo.SetRect(gizmoPos.X, gizmoPos.Y, gizmoSize.X, gizmoSize.Y); + + // Create view and projection matrices for the gizmo + Matrix4x4 view = gizmoView; + Matrix4x4 proj = gizmoProjection; + + // Draw grid + Matrix4x4 identity = Matrix4x4.Identity; + ImGuizmo.DrawGrid(ref view, ref proj, ref identity, 10.0f); + + // IMPORTANT: Use ID management for proper gizmo isolation + ImGuizmo.PushID(0); + + // Draw the gizmo + Matrix4x4 transform = gizmoTransform; + if (ImGuizmo.Manipulate(ref view, ref proj, gizmoOperation, gizmoMode, ref transform)) + { + gizmoTransform = transform; + } + + ImGuizmo.PopID(); + } + + // Display gizmo state below the gizmo area + if (gizmoEnabled) + { + ImGui.Text($"Gizmo Over: {ImGuizmo.IsOver()}"); + ImGui.Text($"Gizmo Using: {ImGuizmo.IsUsing()}"); + } + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/ImNodesDemo.cs b/ImGuiAppDemo/Demos/ImNodesDemo.cs new file mode 100644 index 0000000..6002943 --- /dev/null +++ b/ImGuiAppDemo/Demos/ImNodesDemo.cs @@ -0,0 +1,1365 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; +using Hexa.NET.ImNodes; + +/// +/// Demo for ImNodes node editor +/// +internal sealed class ImNodesDemo : IDemoTab +{ + private int nextNodeId = 1; + private int nextLinkId = 1; + private readonly List nodes = []; + private readonly List links = []; + private bool initialPositionsSet; + private bool automaticLayout; + private readonly Dictionary nodeVelocities = []; + private readonly Dictionary nodeForces = []; + private Vector2? physicsCenter; // Cached center point for physics simulation + private bool showDebugVisualization; // Show physics debug info + + // Debug information + private List linkFixLog = []; + private string linkFixSummary = ""; + + // Physics parameters + private float repulsionStrength = 5000.0f; + private float attractionStrength = 0.5f; + private float centerForce = 0.12f; + private float idealLinkDistance = 200.0f; + private float damping = 0.8f; + private float maxVelocity = 200.0f; + + private sealed record SimpleNode(int Id, Vector2 Position, string Name, List InputPins, List OutputPins, Vector2 Dimensions); + private sealed record SimpleLink(int Id, int InputPinId, int OutputPinId); + + public string TabName => "ImNodes Editor"; + + public ImNodesDemo() + { + // Initialize demo data for ImNodes with better spacing + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(100, 150), "Input Node", [], [1, 2], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 100), "Process Node A", [3], [4, 5], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 250), "Process Node B", [6], [7], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(700, 175), "Output Node", [8, 9], [], Vector2.Zero)); + + // Create some demo links showing a more complex flow + links.Add(new SimpleLink(nextLinkId++, 1, 3)); // Input to Process A + links.Add(new SimpleLink(nextLinkId++, 2, 6)); // Input to Process B + links.Add(new SimpleLink(nextLinkId++, 4, 8)); // Process A to Output + links.Add(new SimpleLink(nextLinkId++, 7, 9)); // Process B to Output + + // Update nextNodeId to account for all the pin IDs we used + nextNodeId = 10; + } + + public void Update(float deltaTime) + { + if (automaticLayout && nodes.Count > 0) + { + // If we just enabled automatic layout, reset positioning flags + if (initialPositionsSet) + { + initialPositionsSet = false; + } + UpdatePhysicsSimulation(deltaTime); + } + else if (!automaticLayout) + { + // Reset physics center when automatic layout is disabled + physicsCenter = null; + } + } + + private void UpdatePhysicsSimulation(float deltaTime) + { + // Initialize forces and velocities for new nodes + foreach (SimpleNode node in nodes) + { + if (!nodeForces.ContainsKey(node.Id)) + { + nodeForces[node.Id] = Vector2.Zero; + nodeVelocities[node.Id] = Vector2.Zero; + } + } + + // Calculate all forces + CalculateForces(); + + // Apply forces and update positions + ApplyForces(deltaTime); + } + + private void CalculateForces() + { + // Reset all forces + foreach (int nodeId in nodeForces.Keys.ToList()) + { + nodeForces[nodeId] = Vector2.Zero; + } + + // Node repulsion forces (nodes push each other away) + for (int i = 0; i < nodes.Count; i++) + { + for (int j = i + 1; j < nodes.Count; j++) + { + SimpleNode nodeA = nodes[i]; + SimpleNode nodeB = nodes[j]; + + // Use node centers for physics calculations + Vector2 centerA = nodeA.Position + (nodeA.Dimensions * 0.5f); + Vector2 centerB = nodeB.Position + (nodeB.Dimensions * 0.5f); + Vector2 direction = centerA - centerB; + float distance = direction.Length(); + + if (distance > 0) + { + // Repulsion force decreases with distance + float force = repulsionStrength / ((distance * distance) + 1.0f); + Vector2 forceVector = Vector2.Normalize(direction) * force; + + nodeForces[nodeA.Id] += forceVector; + nodeForces[nodeB.Id] -= forceVector; + } + } + } + + // Link attraction forces (connected nodes pull toward each other) + foreach (SimpleLink link in links) + { + SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId); + SimpleNode? endNode = GetNodeByInputPin(link.InputPinId); + + if (startNode != null && endNode != null) + { + // Use node centers for physics calculations + Vector2 startCenter = startNode.Position + (startNode.Dimensions * 0.5f); + Vector2 endCenter = endNode.Position + (endNode.Dimensions * 0.5f); + Vector2 direction = endCenter - startCenter; + float distance = direction.Length(); + + if (distance > 0) + { + // Attraction force - stronger for longer links + float force = (distance - idealLinkDistance) * attractionStrength; + Vector2 forceVector = Vector2.Normalize(direction) * force; + + nodeForces[startNode.Id] += forceVector; + nodeForces[endNode.Id] -= forceVector; + } + } + } + + // Center attraction (pull all nodes toward the canvas origin) + // Initialize physics center if not set - always use canvas origin (0,0) + if (!physicsCenter.HasValue) + { + physicsCenter = Vector2.Zero; // Canvas origin (0,0) + } + + // Use the physics center (canvas origin) + Vector2 editorCenter = physicsCenter.Value; + foreach (SimpleNode node in nodes) + { + // Use node center for physics calculations + Vector2 nodeCenter = node.Position + (node.Dimensions * 0.5f); + Vector2 toCenter = editorCenter - nodeCenter; + float distance = toCenter.Length(); + if (distance > 10.0f) // Small deadzone to prevent jittering at center + { + // Centering force that increases with distance from center + float force = distance * centerForce; + nodeForces[node.Id] += Vector2.Normalize(toCenter) * force; + } + } + } + + private void ApplyForces(float deltaTime) + { + + for (int i = 0; i < nodes.Count; i++) + { + SimpleNode node = nodes[i]; + Vector2 force = nodeForces[node.Id]; + + // Update velocity (F = ma, assuming mass = 1) + nodeVelocities[node.Id] += force * deltaTime; + nodeVelocities[node.Id] *= damping; // Apply damping + + // Limit velocity + Vector2 velocity = nodeVelocities[node.Id]; + if (velocity.Length() > maxVelocity) + { + nodeVelocities[node.Id] = Vector2.Normalize(velocity) * maxVelocity; + } + + // Update position + Vector2 newPosition = node.Position + (nodeVelocities[node.Id] * deltaTime); + + // Create updated node and replace in list + nodes[i] = node with { Position = newPosition }; + + // Update ImNodes position in real-time + ImNodes.SetNodeEditorSpacePos(node.Id, newPosition); + } + } + + private SimpleNode? GetNodeByOutputPin(int pinId) => nodes.FirstOrDefault(n => n.OutputPins.Contains(pinId)); + + private SimpleNode? GetNodeByInputPin(int pinId) => nodes.FirstOrDefault(n => n.InputPins.Contains(pinId)); + + private void RenderDebugInformation() + { + ImGui.SeparatorText("Debug Information:"); + + // Get canvas panning once and reuse it throughout debug info + Vector2 canvasPanning = ImNodes.EditorContextGetPanning(); + + RenderGeneralDebugInfo(); + RenderCanvasDebugInfo(canvasPanning); + RenderNodeLayoutDebugInfo(canvasPanning); + + if (automaticLayout) + { + RenderPhysicsDebugInfo(); + } + else + { + RenderBasicNodeDebugInfo(); + } + } + + private void RenderGeneralDebugInfo() + { + ImGui.Text($"Total Nodes: {nodes.Count}"); + ImGui.Text($"Total Links: {links.Count}"); + } + + private static void RenderCanvasDebugInfo(Vector2 canvasPanning) + { + // Canvas panning info (flip Y for intuitive display) + Vector2 displayPanning = new(canvasPanning.X, -canvasPanning.Y); + ImGui.Text($"Origin Offset: ({displayPanning.X:F1}, {displayPanning.Y:F1})"); + ImGui.TextDisabled("(Where origin is relative to center of view)"); + + // Explain what the panning values mean + if (canvasPanning.X == 0.0f && canvasPanning.Y == 0.0f) + { + ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), "✓ Origin at center - (0,0) marker should be visible"); + } + else + { + string direction = GetDirectionString(canvasPanning); + ImGui.TextColored(new Vector4(1.0f, 1.0f, 0.0f, 1.0f), $"Origin is {direction} from center"); + } + } + + private static string GetDirectionString(Vector2 canvasPanning) + { + string direction = ""; + if (canvasPanning.X > 0) + { + direction += "right "; + } + else if (canvasPanning.X < 0) + { + direction += "left "; + } + + if (canvasPanning.Y > 0) + { + direction += "down"; + } + else if (canvasPanning.Y < 0) + { + direction += "up"; + } + + return direction.Trim(); + } + + private void RenderNodeLayoutDebugInfo(Vector2 canvasPanning) + { + if (nodes.Count == 0) + { + return; + } + + // Calculate accurate bounding box using cached node dimensions + SimpleNode firstNode = nodes[0]; + Vector2 firstNodePos = firstNode.Position; + Vector2 firstNodeDims = firstNode.Dimensions; + + Vector2 minPos = firstNodePos; + Vector2 maxPos = firstNodePos + firstNodeDims; + + // Area-weighted center of mass calculation + Vector2 weightedCenterSum = Vector2.Zero; + float totalArea = 0.0f; + + foreach (SimpleNode node in nodes) + { + Vector2 nodePos = node.Position; + Vector2 nodeDims = node.Dimensions; + + // Update bounding box to include full node rectangle + minPos.X = Math.Min(minPos.X, nodePos.X); + minPos.Y = Math.Min(minPos.Y, nodePos.Y); + maxPos.X = Math.Max(maxPos.X, nodePos.X + nodeDims.X); + maxPos.Y = Math.Max(maxPos.Y, nodePos.Y + nodeDims.Y); + + // Area-weighted center of mass + Vector2 nodeCenter = nodePos + (nodeDims * 0.5f); + float nodeArea = nodeDims.X * nodeDims.Y; + weightedCenterSum += nodeCenter * nodeArea; + totalArea += nodeArea; + } + + Vector2 centerOfMass = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero; + + Vector2 boundingSize = maxPos - minPos; + ImGui.Text($"Bounding Box: {boundingSize.X:F1} × {boundingSize.Y:F1} (including node dimensions)"); + Vector2 displayCenterOfMass = new(centerOfMass.X, -centerOfMass.Y); + ImGui.Text($"Center of Mass: ({displayCenterOfMass.X:F1}, {displayCenterOfMass.Y:F1}) (area-weighted)"); + + // Show distances from current view center to key points + float distanceToOrigin = canvasPanning.Length(); + Vector2 centerOffset = centerOfMass - (-canvasPanning); // Center relative to view center + float distanceToCenter = centerOffset.Length(); + + ImGui.Text($"Distance to Origin: {distanceToOrigin:F1}px"); + ImGui.Text($"Distance to Center: {distanceToCenter:F1}px"); + } + + private void RenderPhysicsDebugInfo() + { + // Physics center info + if (physicsCenter.HasValue) + { + Vector2 center = physicsCenter.Value; + Vector2 displayPhysicsCenter = new(center.X, -center.Y); + ImGui.Text($"Physics Center: ({displayPhysicsCenter.X:F1}, {displayPhysicsCenter.Y:F1})"); + } + else + { + ImGui.Text("Physics Center: Not set"); + } + + RenderNodePhysicsData(); + RenderPhysicsLinkDistances(); + } + + private void RenderNodePhysicsData() + { + ImGui.SeparatorText("Node Physics Data:"); + + foreach (SimpleNode node in nodes) + { + ImGui.PushID(node.Id); + + if (ImGui.TreeNode($"Node {node.Id}: {node.Name}")) + { + Vector2 displayNodePos = new(node.Position.X, -node.Position.Y); + ImGui.Text($"Position: ({displayNodePos.X:F1}, {displayNodePos.Y:F1})"); + + if (nodeForces.TryGetValue(node.Id, out Vector2 force)) + { + float forceMagnitude = force.Length(); + Vector2 displayForce = new(force.X, -force.Y); + ImGui.Text($"Force: ({displayForce.X:F2}, {displayForce.Y:F2}) | Mag: {forceMagnitude:F2}"); + } + + if (nodeVelocities.TryGetValue(node.Id, out Vector2 velocity)) + { + float velocityMagnitude = velocity.Length(); + Vector2 displayVelocity = new(velocity.X, -velocity.Y); + ImGui.Text($"Velocity: ({displayVelocity.X:F2}, {displayVelocity.Y:F2}) | Mag: {velocityMagnitude:F2}"); + } + + ImGui.TreePop(); + } + + ImGui.PopID(); + } + } + + private void RenderPhysicsLinkDistances() + { + ImGui.SeparatorText("Link Distances:"); + ImGui.Text($"Total Links: {links.Count}"); + + if (links.Count == 0) + { + ImGui.TextColored(new Vector4(1.0f, 0.7f, 0.0f, 1.0f), "No links found - try creating connections between nodes"); + return; + } + + foreach (SimpleLink link in links) + { + SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId); + SimpleNode? endNode = GetNodeByInputPin(link.InputPinId); + + if (startNode != null && endNode != null) + { + float distance = (endNode.Position - startNode.Position).Length(); + Vector4 color = distance > idealLinkDistance + ? new Vector4(1.0f, 0.3f, 0.3f, 1.0f) // Red if too far + : new Vector4(0.3f, 1.0f, 0.3f, 1.0f); // Green if close enough + + ImGui.TextColored(color, $"Link {link.Id}: {distance:F1}px (ideal: {idealLinkDistance:F0}px)"); + } + else + { + ImGui.TextColored(new Vector4(1.0f, 0.3f, 0.3f, 1.0f), $"Link {link.Id}: ERROR - Missing nodes (Out:{link.OutputPinId} → In:{link.InputPinId})"); + } + } + } + + private void RenderBasicNodeDebugInfo() + { + // Basic node positions (when physics is not active) + if (nodes.Count > 0) + { + ImGui.SeparatorText("Node Positions:"); + + foreach (SimpleNode node in nodes) + { + Vector2 displayNodePos = new(node.Position.X, -node.Position.Y); + ImGui.Text($"Node {node.Id} ({node.Name}): ({displayNodePos.X:F1}, {displayNodePos.Y:F1})"); + } + } + + RenderBasicLinkDistances(); + } + + private void RenderBasicLinkDistances() + { + ImGui.SeparatorText("Link Distances:"); + ImGui.Text($"Total Links: {links.Count}"); + + // Show link fix results if available + if (!string.IsNullOrEmpty(linkFixSummary)) + { + ImGui.SeparatorText("Link Fix Results:"); + ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), linkFixSummary); + + if (linkFixLog.Count > 0) + { + if (ImGui.CollapsingHeader("Fix Details")) + { + foreach (string logEntry in linkFixLog) + { + ImGui.Text(logEntry); + } + } + } + } + + // Debug: Show all node pin configurations + ImGui.SeparatorText("Node Pin Debug:"); + foreach (SimpleNode node in nodes) + { + string inputPins = string.Join(",", node.InputPins); + string outputPins = string.Join(",", node.OutputPins); + ImGui.Text($"Node {node.Id} ({node.Name}): In=[{inputPins}] Out=[{outputPins}]"); + } + + if (links.Count == 0) + { + ImGui.TextColored(new Vector4(1.0f, 0.7f, 0.0f, 1.0f), "No links found - try creating connections between nodes"); + return; + } + + foreach (SimpleLink link in links) + { + SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId); + SimpleNode? endNode = GetNodeByInputPin(link.InputPinId); + + if (startNode != null && endNode != null) + { + float distance = (endNode.Position - startNode.Position).Length(); + ImGui.Text($"Link {link.Id}: {distance:F1}px"); + } + else + { + ImGui.TextColored(new Vector4(1.0f, 0.3f, 0.3f, 1.0f), $"Link {link.Id}: ERROR - Missing nodes (Out:{link.OutputPinId} → In:{link.InputPinId})"); + + // Debug: Show which nodes contain these pins + SimpleNode? nodeWithOutput = nodes.FirstOrDefault(n => n.OutputPins.Contains(link.OutputPinId) || n.InputPins.Contains(link.OutputPinId)); + SimpleNode? nodeWithInput = nodes.FirstOrDefault(n => n.InputPins.Contains(link.InputPinId) || n.OutputPins.Contains(link.InputPinId)); + + if (nodeWithOutput != null) + { + bool isOutput = nodeWithOutput.OutputPins.Contains(link.OutputPinId); + string pinType = isOutput ? "OUTPUT" : "INPUT"; + ImGui.Text($" Pin {link.OutputPinId} found in Node {nodeWithOutput.Id} as {pinType}"); + } + else + { + ImGui.Text($" Pin {link.OutputPinId} not found in any node"); + } + + if (nodeWithInput != null) + { + bool isInput = nodeWithInput.InputPins.Contains(link.InputPinId); + string pinType = isInput ? "INPUT" : "OUTPUT"; + ImGui.Text($" Pin {link.InputPinId} found in Node {nodeWithInput.Id} as {pinType}"); + } + else + { + ImGui.Text($" Pin {link.InputPinId} not found in any node"); + } + } + } + } + + private void RenderAllDebugOverlays(Vector2 editorAreaPos, Vector2 editorAreaSize) + { + // Get window draw list for screen space drawing + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + + // Helper function to convert grid space coordinates to screen space + // For node-relative positions, use reference-based approach + // For absolute world positions, use direct mathematical transformation + Vector2 GridSpaceToScreenSpace(Vector2 gridPos, bool isAbsoluteWorldPosition = false) + { + if (isAbsoluteWorldPosition) + { + // For absolute world positions like origin (0,0) + // Transform directly using ImNodes coordinate system: add panning, not subtract + Vector2 panning = ImNodes.EditorContextGetPanning(); + Vector2 editorCenter = editorAreaPos + (editorAreaSize * 0.5f); + Vector2 screenPos = editorCenter + gridPos + panning; + return screenPos; + } + else if (nodes.Count > 0) + { + // For positions relative to nodes (like bounding box, center of mass) + // Use the first node as a reference to ensure exact coordinate matching + SimpleNode referenceNode = nodes[0]; + Vector2 referenceGridPos = referenceNode.Position; + Vector2 referenceScreenPos = ImNodes.GetNodeScreenSpacePos(referenceNode.Id); + + // Calculate the offset from the reference node in grid space + Vector2 offset = gridPos - referenceGridPos; + + // Apply the same offset in screen space + return referenceScreenPos + offset; + } + else + { + // Fallback for when no nodes exist + Vector2 panning = ImNodes.EditorContextGetPanning(); + Vector2 editorCenter = editorAreaPos + (editorAreaSize * 0.5f); + Vector2 screenPos = editorCenter + gridPos + panning; + return screenPos; + } + } + + // Colors + uint forceColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.5f, 0.0f, 0.8f)); // Orange + uint velocityColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 1.0f, 0.5f, 0.8f)); // Green + uint centerColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.0f, 1.0f, 0.8f)); // Magenta + uint repulsionColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.0f, 0.0f, 0.3f)); // Red + uint originColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.8f, 1.0f, 1.0f)); // Cyan + uint boundingBoxColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 0.0f, 0.6f)); // Yellow + uint centerOfMassColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 1.0f, 1.0f, 0.9f)); // Cyan + + // Draw canvas origin (0,0) marker + Vector2 originScreen = GridSpaceToScreenSpace(new Vector2(0.0f, 0.0f), isAbsoluteWorldPosition: true); + drawList.AddLine(originScreen + new Vector2(-50, 0), originScreen + new Vector2(50, 0), originColor, 4.0f); + drawList.AddLine(originScreen + new Vector2(0, -50), originScreen + new Vector2(0, 50), originColor, 4.0f); + drawList.AddCircle(originScreen, 15.0f, originColor, 16, 3.0f); + drawList.AddCircle(originScreen, 25.0f, originColor, 16, 2.0f); + drawList.AddCircleFilled(originScreen, 5.0f, originColor); + drawList.AddText(originScreen + new Vector2(30, -15), originColor, "ORIGIN (0,0)"); + + // Physics-specific debug elements (only when physics is active) + if (automaticLayout) + { + // Draw physics center + if (physicsCenter.HasValue) + { + Vector2 centerScreen = GridSpaceToScreenSpace(physicsCenter.Value, isAbsoluteWorldPosition: false); + drawList.AddCircleFilled(centerScreen, 8.0f, centerColor); + drawList.AddText(centerScreen + new Vector2(10, -5), centerColor, "Physics Center"); + } + + // Draw forces and velocities for each node + foreach (SimpleNode node in nodes) + { + // Draw from node center for accurate physics visualization + Vector2 nodeCenter = node.Position + (node.Dimensions * 0.5f); + Vector2 nodeCenterScreen = GridSpaceToScreenSpace(nodeCenter); + + // Draw force vector (scaled for visibility) + if (nodeForces.TryGetValue(node.Id, out Vector2 force)) + { + Vector2 forceEndScreen = GridSpaceToScreenSpace(nodeCenter + (force * 0.01f)); // Scale down for visibility + if (force.Length() > 0.1f) // Only draw if significant + { + drawList.AddLine(nodeCenterScreen, forceEndScreen, forceColor, 2.0f); + drawList.AddCircleFilled(forceEndScreen, 3.0f, forceColor); + } + } + + // Draw velocity vector (scaled for visibility) + if (nodeVelocities.TryGetValue(node.Id, out Vector2 velocity)) + { + Vector2 velocityEndScreen = GridSpaceToScreenSpace(nodeCenter + (velocity * 0.1f)); // Scale down for visibility + if (velocity.Length() > 0.1f) // Only draw if significant + { + drawList.AddLine(nodeCenterScreen, velocityEndScreen, velocityColor, 2.0f); + drawList.AddCircleFilled(velocityEndScreen, 3.0f, velocityColor); + } + } + + // Draw repulsion zones (faint circles) + drawList.AddCircle(nodeCenterScreen, 100.0f, repulsionColor, 32, 1.0f); + } + } + + // Draw bounding box and center of mass for all nodes + if (nodes.Count > 0) + { + // Calculate accurate bounding box using cached node dimensions and area-weighted center of mass + SimpleNode firstNode = nodes[0]; + Vector2 firstNodePos = firstNode.Position; + Vector2 firstNodeDims = firstNode.Dimensions; + + // Initialize bounding box with first node's full rectangle + Vector2 minPosGrid = firstNodePos; + Vector2 maxPosGrid = firstNodePos + firstNodeDims; + + // For area-weighted center of mass calculation + Vector2 weightedCenterSum = Vector2.Zero; + float totalArea = 0.0f; + + // Calculate accurate bounding box and area-weighted center of mass + foreach (SimpleNode node in nodes) + { + Vector2 nodePos = node.Position; + Vector2 nodeDims = node.Dimensions; + + // Update bounding box to include full node rectangle + minPosGrid.X = Math.Min(minPosGrid.X, nodePos.X); + minPosGrid.Y = Math.Min(minPosGrid.Y, nodePos.Y); + maxPosGrid.X = Math.Max(maxPosGrid.X, nodePos.X + nodeDims.X); + maxPosGrid.Y = Math.Max(maxPosGrid.Y, nodePos.Y + nodeDims.Y); + + // Calculate area-weighted center of mass (center of each node weighted by its area) + Vector2 nodeCenter = nodePos + (nodeDims * 0.5f); + float nodeArea = nodeDims.X * nodeDims.Y; + weightedCenterSum += nodeCenter * nodeArea; + totalArea += nodeArea; + } + + // Convert to screen coordinates + Vector2 minPosScreen = GridSpaceToScreenSpace(minPosGrid); + Vector2 maxPosScreen = GridSpaceToScreenSpace(maxPosGrid); + + // Calculate final area-weighted center of mass + Vector2 centerOfMassGrid = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero; + Vector2 centerOfMassScreen = GridSpaceToScreenSpace(centerOfMassGrid); + + // Draw accurate bounding box + drawList.AddRect(minPosScreen, maxPosScreen, boundingBoxColor, 0.0f, ImDrawFlags.None, 2.0f); + drawList.AddText(minPosScreen + new Vector2(5, -20), boundingBoxColor, "Bounding Box"); + + // Draw area-weighted center of mass + drawList.AddCircleFilled(centerOfMassScreen, 6.0f, centerOfMassColor); + drawList.AddCircle(centerOfMassScreen, 12.0f, centerOfMassColor, 16, 2.0f); + drawList.AddText(centerOfMassScreen + new Vector2(15, -8), centerOfMassColor, "Center of Mass"); + } + + // Draw link connections and distances (always when debug visualization is on) + foreach (SimpleLink link in links) + { + SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId); + SimpleNode? endNode = GetNodeByInputPin(link.InputPinId); + + if (startNode != null && endNode != null) + { + Vector2 startScreen = GridSpaceToScreenSpace(startNode.Position); + Vector2 endScreen = GridSpaceToScreenSpace(endNode.Position); + float distance = (endNode.Position - startNode.Position).Length(); + + // Color based on distance vs ideal (only when physics is enabled) + Vector4 color; + if (automaticLayout) + { + color = distance > idealLinkDistance + ? new Vector4(1.0f, 0.0f, 0.0f, 0.5f) // Red if too far + : new Vector4(0.0f, 1.0f, 0.0f, 0.5f); // Green if close enough + } + else + { + // Use neutral blue color when physics is off + color = new Vector4(0.3f, 0.7f, 1.0f, 0.6f); // Light blue + } + + uint lineColor = ImGui.ColorConvertFloat4ToU32(color); + drawList.AddLine(startScreen, endScreen, lineColor, 1.0f); + + // Draw distance text at midpoint + Vector2 midpointScreen = (startScreen + endScreen) * 0.5f; + string distanceText = automaticLayout ? $"{distance:F0}px" : $"{distance:F0}px"; + drawList.AddText(midpointScreen, lineColor, distanceText); + } + } + + // Draw compass (always visible when debug is on and not at origin) + Vector2 currentPanning = ImNodes.EditorContextGetPanning(); + if (currentPanning.X != 0.0f || currentPanning.Y != 0.0f) + { + Vector2 directionToOrigin = currentPanning; + float distance = directionToOrigin.Length(); + + if (distance > 10.0f) // Only show if we're not too close to origin + { + Vector2 normalizedDirection = Vector2.Normalize(directionToOrigin); + + // Position compass in the center of the editor area in screen space + Vector2 compassCenter = editorAreaPos + (editorAreaSize * 0.5f); + + // Colors + uint compassBgColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f)); + uint compassArrowColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.3f, 0.3f, 1.0f)); // Red arrow + + // Draw compass background circle + drawList.AddCircleFilled(compassCenter, 35.0f, compassBgColor); + drawList.AddCircle(compassCenter, 35.0f, originColor, 32, 2.0f); + + // Draw compass arrow pointing to origin + Vector2 arrowEnd = compassCenter + (normalizedDirection * 25.0f); + Vector2 arrowLeft = compassCenter + (new Vector2(-normalizedDirection.Y, normalizedDirection.X) * 8.0f) + (normalizedDirection * 15.0f); + Vector2 arrowRight = compassCenter + (new Vector2(normalizedDirection.Y, -normalizedDirection.X) * 8.0f) + (normalizedDirection * 15.0f); + + // Draw arrow shaft + drawList.AddLine(compassCenter, arrowEnd, compassArrowColor, 3.0f); + // Draw arrow head + drawList.AddTriangleFilled(arrowEnd, arrowLeft, arrowRight, compassArrowColor); + + // Add distance text + drawList.AddText(compassCenter + new Vector2(-15, 40), originColor, $"{distance:F0}px"); + drawList.AddText(compassCenter + new Vector2(-20, -50), originColor, "TO ORIGIN"); + } + } + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + RenderHeader(); + RenderControls(); + RenderNodeEditor(); + HandleLinkEvents(); + + ImGui.EndTabItem(); + } + } + + private static void RenderHeader() + { + ImGui.TextWrapped("ImNodes provides a node editor with support for nodes, pins, and connections."); + ImGui.Separator(); + } + + private void RenderControls() + { + if (ImGui.Button("Add Node")) + { + // Place new nodes in a grid pattern to avoid overlap + int nodeIndex = nodes.Count; + int row = nodeIndex / 3; + int col = nodeIndex % 3; + Vector2 nodePos = new(150 + (col * 250), 100 + (row * 150)); + + nodes.Add(new SimpleNode( + nextNodeId++, + nodePos, + $"Custom Node {nodeIndex + 1}", + [nextNodeId, nextNodeId + 1], // Input pins + [nextNodeId + 2, nextNodeId + 3], // Output pins + Vector2.Zero // Dimensions will be updated after rendering +)); + nextNodeId += 4; // Reserve IDs for pins + } + + ImGui.SameLine(); + if (ImGui.Button("Clear All")) + { + ClearAllNodes(); + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Demo")) + { + ResetToDemo(); + } + + ImGui.SameLine(); + if (ImGui.Button("Fix Links")) + { + FixCorruptedLinks(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Fix any corrupted links with incorrect pin mappings"); + } + + if (!string.IsNullOrEmpty(linkFixSummary)) + { + ImGui.SameLine(); + if (ImGui.Button("Clear Log")) + { + linkFixLog.Clear(); + linkFixSummary = ""; + } + } + } + + private void ClearAllNodes() + { + nodes.Clear(); + links.Clear(); + nodeVelocities.Clear(); + nodeForces.Clear(); + physicsCenter = null; + nextNodeId = 1; + nextLinkId = 1; + initialPositionsSet = false; + } + + private void ResetToDemo() + { + ClearAllNodes(); + + // Recreate the demo layout + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(100, 150), "Input Node", [], [1, 2], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 100), "Process Node A", [3], [4, 5], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 250), "Process Node B", [6], [7], Vector2.Zero)); + nodes.Add(new SimpleNode(nextNodeId++, new Vector2(700, 175), "Output Node", [8, 9], [], Vector2.Zero)); + + links.Add(new SimpleLink(nextLinkId++, 1, 3)); // Input to Process A + links.Add(new SimpleLink(nextLinkId++, 2, 6)); // Input to Process B + links.Add(new SimpleLink(nextLinkId++, 4, 8)); // Process A to Output + links.Add(new SimpleLink(nextLinkId++, 7, 9)); // Process B to Output + + nextNodeId = 10; + } + + private void FixCorruptedLinks() + { + List fixedLinks = []; + int originalCount = links.Count; + int fixedCount = 0; + int correctCount = 0; + List fixLog = []; + + foreach (SimpleLink link in links) + { + fixLog.Add($"Processing Link {link.Id}: Out:{link.OutputPinId} → In:{link.InputPinId}"); + + // Find nodes containing these pins + SimpleNode? nodeWithOutputPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(link.OutputPinId) || n.InputPins.Contains(link.OutputPinId)); + SimpleNode? nodeWithInputPin = nodes.FirstOrDefault(n => n.InputPins.Contains(link.InputPinId) || n.OutputPins.Contains(link.InputPinId)); + + fixLog.Add($" nodeWithOutputPin: {nodeWithOutputPin?.Name} (ID: {nodeWithOutputPin?.Id})"); + fixLog.Add($" nodeWithInputPin: {nodeWithInputPin?.Name} (ID: {nodeWithInputPin?.Id})"); + + if (nodeWithOutputPin != null && nodeWithInputPin != null) + { + bool outputPinIsActuallyOutput = nodeWithOutputPin.OutputPins.Contains(link.OutputPinId); + bool inputPinIsActuallyInput = nodeWithInputPin.InputPins.Contains(link.InputPinId); + + fixLog.Add($" outputPinIsActuallyOutput: {outputPinIsActuallyOutput}"); + fixLog.Add($" inputPinIsActuallyInput: {inputPinIsActuallyInput}"); + + if (outputPinIsActuallyOutput && inputPinIsActuallyInput) + { + // Link is correct + fixLog.Add($" Link {link.Id} is already correct"); + fixedLinks.Add(link); + correctCount++; + } + else + { + // Try to fix the link by finding the correct pin mappings + int actualOutputPin = -1; + int actualInputPin = -1; + + // Check if pins are swapped + bool canSwap = nodeWithOutputPin.InputPins.Contains(link.OutputPinId) && nodeWithInputPin.OutputPins.Contains(link.InputPinId); + fixLog.Add($" Can swap pins: {canSwap}"); + + if (canSwap) + { + // Pins are swapped + actualOutputPin = link.InputPinId; + actualInputPin = link.OutputPinId; + fixedCount++; + fixLog.Add($" Original: Out:{link.OutputPinId} → In:{link.InputPinId}"); + fixLog.Add($" Fixed to: Out:{actualOutputPin} → In:{actualInputPin}"); + } + + if (actualOutputPin != -1 && actualInputPin != -1) + { + // Create corrected link + fixedLinks.Add(new SimpleLink(link.Id, actualOutputPin, actualInputPin)); + } + else + { + fixLog.Add($" Link {link.Id} could not be fixed"); + } + } + } + else + { + fixLog.Add($" Link {link.Id} - nodes not found, removing link"); + } + } + + // Replace the links collection with fixed links + links.Clear(); + links.AddRange(fixedLinks); + + // Add final verification + fixLog.Add(""); + fixLog.Add("Final links after fix:"); + foreach (SimpleLink finalLink in links) + { + fixLog.Add($" Link {finalLink.Id}: Out:{finalLink.OutputPinId} → In:{finalLink.InputPinId}"); + } + + // Store the fix results for display + linkFixLog = fixLog; + linkFixSummary = $"Link Fix Results: {originalCount} original, {correctCount} already correct, {fixedCount} fixed, {fixedLinks.Count} total after fix"; + } + + private void RenderNodeEditor() + { + // Create horizontal layout: editor on left, parameters on right + ImGui.BeginTable("NodeEditorLayout", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); + ImGui.TableSetupColumn("Editor", ImGuiTableColumnFlags.WidthStretch, 0.75f); + ImGui.TableSetupColumn("Parameters", ImGuiTableColumnFlags.WidthStretch, 0.25f); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + + // Store editor area position and size for compass rendering + Vector2 editorAreaPos = ImGui.GetCursorScreenPos(); + Vector2 editorAreaSize = ImGui.GetContentRegionAvail(); + + // Node editor + ImNodes.BeginNodeEditor(); + + // Set initial positions only once (skip if automatic layout is active) + if (!initialPositionsSet && nodes.Count > 0 && !automaticLayout) + { + foreach (SimpleNode node in nodes) + { + ImNodes.SetNodeEditorSpacePos(node.Id, node.Position); + } + initialPositionsSet = true; + } + + RenderNodes(); + RenderLinks(); + + // Sync node positions and dimensions from ImNodes back to our data structure + // This ensures manual node dragging works properly and keeps dimensions cached + for (int i = 0; i < nodes.Count; i++) + { + SimpleNode node = nodes[i]; + Vector2 currentImNodesPos = ImNodes.GetNodeEditorSpacePos(node.Id); + Vector2 currentImNodesDims = ImNodes.GetNodeDimensions(node.Id); + + // Update if position has changed or dimensions are not cached yet + bool positionChanged = Vector2.Distance(node.Position, currentImNodesPos) > 0.1f; + bool dimensionsNotCached = node.Dimensions == Vector2.Zero; + bool dimensionsChanged = Vector2.Distance(node.Dimensions, currentImNodesDims) > 0.1f; + + if (positionChanged || dimensionsNotCached || dimensionsChanged) + { + nodes[i] = node with { Position = currentImNodesPos, Dimensions = currentImNodesDims }; + } + } + + ImNodes.EndNodeEditor(); + + // Move to the parameters column + ImGui.TableNextColumn(); + RenderParametersPanel(); + + ImGui.EndTable(); + + // Draw all debug visualization after everything else so it appears on top + if (showDebugVisualization) + { + RenderAllDebugOverlays(editorAreaPos, editorAreaSize); + } + } + + private void RenderNodes() + { + // Render nodes + for (int i = 0; i < nodes.Count; i++) + { + SimpleNode node = nodes[i]; + ImNodes.BeginNode(node.Id); + + // Node title + ImNodes.BeginNodeTitleBar(); + ImGui.TextUnformatted(node.Name); + ImNodes.EndNodeTitleBar(); + + // Input pins + for (int j = 0; j < node.InputPins.Count; j++) + { + int pinId = node.InputPins[j]; + ImNodes.BeginInputAttribute(pinId); + ImGui.Text($"In {j + 1}"); + ImNodes.EndInputAttribute(); + } + + // Node content + ImGui.Text($"Node ID: {node.Id}"); + + // Output pins + for (int j = 0; j < node.OutputPins.Count; j++) + { + int pinId = node.OutputPins[j]; + ImNodes.BeginOutputAttribute(pinId); + ImGui.Indent(40); + ImGui.Text($"Out {j + 1}"); + ImNodes.EndOutputAttribute(); + } + + ImNodes.EndNode(); + } + } + + private void RenderLinks() + { + // Render links + foreach (SimpleLink link in links) + { + ImNodes.Link(link.Id, link.InputPinId, link.OutputPinId); + } + } + + private void RenderParametersPanel() + { + // Debug Visualization (always available) + ImGui.SeparatorText("Debug Visualization:"); + ImGui.Checkbox("Show Debug Visualization", ref showDebugVisualization); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Show visual overlays: canvas origin, node bounding box, center of mass, and physics data (if enabled)"); + } + + // Show debug information when enabled + if (showDebugVisualization) + { + RenderDebugInformation(); + } + + ImGui.SeparatorText("Physics Layout:"); + + // Physics layout toggle + ImGui.Checkbox("Automatic Layout", ref automaticLayout); + ImGui.SameLine(); + ImGui.TextDisabled("(?)"); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Physics simulation: node repulsion, link attraction, and center gravity"); + } + + if (automaticLayout) + { + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), "● ACTIVE"); + } + + // Physics parameters panel + if (automaticLayout) + { + ImGui.SeparatorText("Physics Parameters:"); + + RenderPhysicsInputs(); + RenderPhysicsInfo(); + } + else + { + ImGui.TextDisabled("Enable Automatic Layout above"); + ImGui.TextDisabled("to show physics parameters"); + } + + // Canvas Navigation Controls + ImGui.SeparatorText("Canvas Navigation:"); + + if (ImGui.Button("Reset Canvas to Origin")) + { + // Reset canvas panning to origin (0,0) + ImNodes.EditorContextResetPanning(new Vector2(0.0f, 0.0f)); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Pan the canvas so origin (0,0) is visible"); + } + + if (ImGui.Button("Center Canvas on Nodes")) + { + // Calculate area-weighted center of mass of all nodes using cached dimensions + if (nodes.Count > 0) + { + Vector2 weightedCenterSum = Vector2.Zero; + float totalArea = 0.0f; + + foreach (SimpleNode node in nodes) + { + // Use cached position and dimensions for efficient calculation + Vector2 nodePos = node.Position; + Vector2 nodeDims = node.Dimensions; + + // Calculate area-weighted center (center of each node weighted by its area) + Vector2 nodeCenter = nodePos + (nodeDims * 0.5f); + float nodeArea = nodeDims.X * nodeDims.Y; + weightedCenterSum += nodeCenter * nodeArea; + totalArea += nodeArea; + } + + Vector2 centerOfMass = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero; + + // Pan canvas to center the area-weighted center of mass + ImNodes.EditorContextResetPanning(centerOfMass); + } + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Pan the canvas to center all nodes in view"); + } + } + + private void RenderPhysicsInputs() + { + ImGui.InputFloat("Repulsion Strength", ref repulsionStrength, 100.0f, 1000.0f, "%.0f"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("REPULSION FORCE IMPLEMENTATION:\n" + + "â€ĸ Formula: force = repulsionStrength / ((distance²) + 1)\n" + + "â€ĸ Applied between ALL node pairs (N² complexity)\n" + + "â€ĸ Prevents nodes from overlapping\n" + + "â€ĸ Higher values = stronger push-apart force\n" + + "â€ĸ Visible as: Red circles around nodes\n" + + "â€ĸ Default: 5000 (good balance for most layouts)"); + } + else + { + ImGui.SetTooltip("How strongly nodes push each other away"); + } + } + + ImGui.InputFloat("Attraction Strength", ref attractionStrength, 0.01f, 0.1f, "%.3f"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("ATTRACTION FORCE IMPLEMENTATION:\n" + + "â€ĸ Formula: force = (distance - idealLinkDistance) × attractionStrength\n" + + "â€ĸ Applied only between CONNECTED nodes\n" + + "â€ĸ Pulls connected nodes toward ideal distance\n" + + "â€ĸ Spring-like behavior: stronger when farther from ideal\n" + + "â€ĸ Visible as: Green/Red lines between connected nodes\n" + + "â€ĸ Default: 0.5 (moderate spring strength)"); + } + else + { + ImGui.SetTooltip("How strongly connected nodes pull toward each other"); + } + } + + ImGui.InputFloat("Center Force", ref centerForce, 0.001f, 0.01f, "%.4f"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("CENTER GRAVITY IMPLEMENTATION:\n" + + "â€ĸ Formula: force = distance × centerForce\n" + + "â€ĸ Applied to ALL nodes toward physics center\n" + + "â€ĸ Prevents nodes from drifting to edges\n" + + "â€ĸ Linear increase with distance from center\n" + + "â€ĸ Physics center = canvas origin (0,0)\n" + + "â€ĸ Visible as: Magenta circle at origin\n" + + "â€ĸ Default: 0.12 (gentle centering force)"); + } + else + { + ImGui.SetTooltip("How strongly nodes are pulled toward the center"); + } + } + + ImGui.InputFloat("Ideal Link Distance", ref idealLinkDistance, 10.0f, 50.0f, "%.0f px"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("IDEAL DISTANCE IMPLEMENTATION:\n" + + "â€ĸ Target distance for connected nodes\n" + + "â€ĸ Used in attraction force calculation\n" + + "â€ĸ Links shorter than ideal: nodes pull apart\n" + + "â€ĸ Links longer than ideal: nodes pull together\n" + + "â€ĸ Visible as: Distance labels on connections\n" + + "â€ĸ Green lines = close to ideal, Red lines = too far\n" + + "â€ĸ Default: 200px (good for typical node sizes)"); + } + else + { + ImGui.SetTooltip("Preferred distance between connected nodes"); + } + } + + ImGui.InputFloat("Damping", ref damping, 0.01f, 0.1f, "%.3f"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("VELOCITY DAMPING IMPLEMENTATION:\n" + + "â€ĸ Formula: velocity = velocity × damping (each frame)\n" + + "â€ĸ Applied to ALL nodes every simulation step\n" + + "â€ĸ Simulates friction/air resistance\n" + + "â€ĸ Higher values = nodes slow down faster\n" + + "â€ĸ Prevents oscillation and ensures convergence\n" + + "â€ĸ Visible as: Green arrows (velocity vectors)\n" + + "â€ĸ Range: 0.1-0.95 (0.8 = good stability)"); + } + else + { + ImGui.SetTooltip("How quickly nodes slow down (higher = more stable)"); + } + } + + ImGui.InputFloat("Max Velocity", ref maxVelocity, 10.0f, 100.0f, "%.0f px/s"); + if (ImGui.IsItemHovered()) + { + if (showDebugVisualization) + { + ImGui.SetTooltip("VELOCITY LIMITING IMPLEMENTATION:\n" + + "â€ĸ Applied after force integration each frame\n" + + "â€ĸ Prevents explosive behavior with high forces\n" + + "â€ĸ Clamps velocity magnitude to this maximum\n" + + "â€ĸ Maintains velocity direction, only limits speed\n" + + "â€ĸ Essential for simulation stability\n" + + "â€ĸ Visible as: Length of green velocity arrows\n" + + "â€ĸ Default: 200px/s (smooth but responsive)"); + } + else + { + ImGui.SetTooltip("Maximum speed limit for node movement"); + } + } + + if (ImGui.Button("Reset Parameters")) + { + repulsionStrength = 5000.0f; + attractionStrength = 0.5f; + centerForce = 0.12f; + idealLinkDistance = 200.0f; + damping = 0.8f; + maxVelocity = 200.0f; + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Restore default values"); + } + } + + private void RenderPhysicsInfo() + { + // Display current physics info + ImGui.SeparatorText("Physics Info:"); + ImGui.Text($"Active Nodes: {nodes.Count}"); + if (physicsCenter.HasValue) + { + Vector2 center = physicsCenter.Value; + ImGui.Text($"Center: ({center.X:F0}, {center.Y:F0})"); + } + + // Show total system energy (sum of all velocities) + float totalEnergy = 0.0f; + foreach (KeyValuePair kvp in nodeVelocities) + { + totalEnergy += kvp.Value.LengthSquared(); + } + ImGui.Text($"System Energy: {totalEnergy:F2}"); + } + + private void HandleLinkEvents() + { + // Handle new links + bool isLinkCreated; + int startPin = 0; + int endPin = 0; + + unsafe + { + isLinkCreated = ImNodes.IsLinkCreated(&startPin, &endPin); + } + + if (isLinkCreated) + { + // Copy to local variables for use in lambdas + int startPinId = startPin; + int endPinId = endPin; + + // Determine which pin is output and which is input + SimpleNode? nodeWithStartPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(startPinId) || n.InputPins.Contains(startPinId)); + SimpleNode? nodeWithEndPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(endPinId) || n.InputPins.Contains(endPinId)); + + if (nodeWithStartPin != null && nodeWithEndPin != null) + { + bool startIsOutput = nodeWithStartPin.OutputPins.Contains(startPinId); + bool endIsOutput = nodeWithEndPin.OutputPins.Contains(endPinId); + + // Create link with correct pin order: (linkId, outputPinId, inputPinId) + if (startIsOutput && !endIsOutput) + { + // Start pin is output, end pin is input - correct order + links.Add(new SimpleLink(nextLinkId++, startPinId, endPinId)); + } + else if (!startIsOutput && endIsOutput) + { + // Start pin is input, end pin is output - reverse order + links.Add(new SimpleLink(nextLinkId++, endPinId, startPinId)); + } + // If both are same type (output-output or input-input), don't create the link + } + } + + // Handle link destruction + bool isLinkDestroyed; + int linkId = 0; + int safeLinkId = 0; + + unsafe + { + isLinkDestroyed = ImNodes.IsLinkDestroyed(&linkId); + safeLinkId = linkId; // Store the link ID safely + } + + if (isLinkDestroyed) + { + links.RemoveAll(link => link.Id == safeLinkId); + } + } +} diff --git a/ImGuiAppDemo/Demos/ImPlotDemo.cs b/ImGuiAppDemo/Demos/ImPlotDemo.cs new file mode 100644 index 0000000..34b2ea5 --- /dev/null +++ b/ImGuiAppDemo/Demos/ImPlotDemo.cs @@ -0,0 +1,146 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; +using Hexa.NET.ImPlot; + +/// +/// Demo for ImPlot advanced plotting +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")] +internal sealed class ImPlotDemo : IDemoTab +{ + private readonly List sinData = []; + private readonly List cosData = []; + private readonly List noiseData = []; + private readonly List plotValues = []; + private float plotTime; + private readonly Random plotRandom = new(); + private float plotRefreshTime; + + public string TabName => "ImPlot Charts"; + + public ImPlotDemo() + { + // Initialize plot data + for (int i = 0; i < 100; i++) + { + float x = i * 0.1f; + sinData.Add(MathF.Sin(x)); + cosData.Add(MathF.Cos(x)); + noiseData.Add((float)((plotRandom.NextDouble() * 2.0) - 1.0)); + } + } + + public void Update(float deltaTime) + { + plotTime += deltaTime; + + // Update plot data for real-time demo + plotRefreshTime += deltaTime; + if (plotRefreshTime >= 0.1f) // Update every 100ms + { + plotRefreshTime = 0; + plotValues.Add((float)plotRandom.NextDouble()); + if (plotValues.Count > 100) // Keep last 100 values + { + plotValues.RemoveAt(0); + } + } + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.TextWrapped("ImPlot provides advanced plotting capabilities with various chart types."); + ImGui.Separator(); + + // Plot controls + if (ImGui.Button("Generate New Data")) + { + sinData.Clear(); + cosData.Clear(); + noiseData.Clear(); + + for (int i = 0; i < 100; i++) + { + float x = i * 0.1f; + sinData.Add(MathF.Sin(x + plotTime)); + cosData.Add(MathF.Cos(x + plotTime)); + noiseData.Add((float)((plotRandom.NextDouble() * 2.0) - 1.0)); + } + } + + ImGui.Separator(); + + // Line plot + if (ImPlot.BeginPlot("Trigonometric Functions", new Vector2(-1, 200))) + { + unsafe + { + fixed (float* sinPtr = sinData.ToArray()) + fixed (float* cosPtr = cosData.ToArray()) + { + ImPlot.PlotLine("sin(x)", sinPtr, sinData.Count); + ImPlot.PlotLine("cos(x)", cosPtr, cosData.Count); + } + } + ImPlot.EndPlot(); + } + + // Scatter plot + if (ImPlot.BeginPlot("Noise Data (Scatter)", new Vector2(-1, 200))) + { + unsafe + { + fixed (float* noisePtr = noiseData.ToArray()) + { + ImPlot.PlotScatter("Random Noise", noisePtr, noiseData.Count); + } + } + ImPlot.EndPlot(); + } + + // Bar chart + if (ImPlot.BeginPlot("Sample Bar Chart", new Vector2(-1, 200))) + { + float[] barData = [1.0f, 2.5f, 3.2f, 1.8f, 4.1f, 2.9f, 3.6f]; + unsafe + { + fixed (float* barPtr = barData) + { + ImPlot.PlotBars("Values", barPtr, barData.Length); + } + } + ImPlot.EndPlot(); + } + + // Real-time plot + if (ImPlot.BeginPlot("Real-time Data", new Vector2(-1, 200))) + { + // Update real-time data + if (plotValues.Count > 0) + { + unsafe + { + fixed (float* plotPtr = plotValues.ToArray()) + { + ImPlot.PlotLine("Live Data", plotPtr, plotValues.Count); + } + } + } + ImPlot.EndPlot(); + } + + ImGui.Text($"Plot Time: {plotTime:F2}"); + ImGui.Text($"Data Points: Sin({sinData.Count}), Cos({cosData.Count}), Noise({noiseData.Count})"); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/InputHandlingDemo.cs b/ImGuiAppDemo/Demos/InputHandlingDemo.cs new file mode 100644 index 0000000..a12be6f --- /dev/null +++ b/ImGuiAppDemo/Demos/InputHandlingDemo.cs @@ -0,0 +1,152 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using System.Text; +using Hexa.NET.ImGui; + +/// +/// Demo for input handling and interaction +/// +internal sealed class InputHandlingDemo : IDemoTab +{ + private readonly StringBuilder textBuffer = new(1024); + private bool wrapText = true; + private bool showModal; + private bool showPopup; + private string modalResult = ""; + private string modalInputBuffer = ""; + + public string TabName => "Input & Interaction"; + + public InputHandlingDemo() + { + textBuffer.Append("This is a demonstration of ImGui text editing capabilities.\n"); + textBuffer.Append("You can edit this text, and it will update in real-time.\n"); + textBuffer.Append("ImGui supports multi-line text editing with syntax highlighting possibilities."); + } + + public void Update(float deltaTime) => + // Handle modals and popups + HandleModalAndPopups(); + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.SeparatorText("Mouse Information:"); + Vector2 mousePos = ImGui.GetMousePos(); + Vector2 mouseDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Left); + ImGui.Text($"Mouse Position: ({mousePos.X:F1}, {mousePos.Y:F1})"); + ImGui.Text($"Mouse Delta: ({mouseDelta.X:F1}, {mouseDelta.Y:F1})"); + ImGui.Text($"Left Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Left) ? "DOWN" : "UP")}"); + ImGui.Text($"Right Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Right) ? "DOWN" : "UP")}"); + + // Simple drag demonstration + ImGui.SeparatorText("Drag & Drop:"); + ImGui.Button("Drag Source", new Vector2(100, 50)); + ImGui.SameLine(); + ImGui.Button("Drop Target", new Vector2(100, 50)); + ImGui.Text("(Drag and drop functionality would require more complex implementation)"); + + // Text editing + ImGui.SeparatorText("Multi-line Text Editor:"); + ImGui.Checkbox("Word Wrap", ref wrapText); + ImGuiInputTextFlags textFlags = ImGuiInputTextFlags.AllowTabInput; + if (!wrapText) + { + textFlags |= ImGuiInputTextFlags.NoHorizontalScroll; + } + + string textContent = textBuffer.ToString(); + if (ImGui.InputTextMultiline("##TextEditor", ref textContent, 1024, new Vector2(-1, 150), textFlags)) + { + textBuffer.Clear(); + textBuffer.Append(textContent); + } + + // Popup and modal buttons + ImGui.SeparatorText("Popups and Modals:"); + if (ImGui.Button("Show Modal")) + { + showModal = true; + modalResult = ""; + } + + ImGui.SameLine(); + if (ImGui.Button("Show Popup")) + { + showPopup = true; + } + + if (!string.IsNullOrEmpty(modalResult)) + { + ImGui.Text($"Modal Result: {modalResult}"); + } + + ImGui.EndTabItem(); + } + } + + private void HandleModalAndPopups() + { + // Modal dialog + if (showModal) + { + ImGui.OpenPopup("Demo Modal"); + showModal = false; + } + + if (ImGui.BeginPopupModal("Demo Modal", ref showModal)) + { + ImGui.Text("This is a modal dialog."); + ImGui.Text("It blocks interaction with the main window."); + ImGui.Separator(); + + ImGui.InputText("Input", ref modalInputBuffer, 100); + + if (ImGui.Button("OK")) + { + modalResult = $"You entered: {modalInputBuffer}"; + ImGui.CloseCurrentPopup(); + } + ImGui.SameLine(); + if (ImGui.Button("Cancel")) + { + modalResult = "Cancelled"; + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + + // Context popup + if (showPopup) + { + ImGui.OpenPopup("Demo Popup"); + showPopup = false; + } + + if (ImGui.BeginPopup("Demo Popup")) + { + ImGui.Text("This is a popup menu"); + if (ImGui.MenuItem("Option 1")) + { + // Handle option 1 + } + if (ImGui.MenuItem("Option 2")) + { + // Handle option 2 + } + ImGui.Separator(); + if (ImGui.MenuItem("Close")) + { + // Handle close + } + ImGui.EndPopup(); + } + } +} diff --git a/ImGuiAppDemo/Demos/LayoutDemo.cs b/ImGuiAppDemo/Demos/LayoutDemo.cs new file mode 100644 index 0000000..8c96b5b --- /dev/null +++ b/ImGuiAppDemo/Demos/LayoutDemo.cs @@ -0,0 +1,135 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Numerics; +using Hexa.NET.ImGui; + +/// +/// Demo for layout systems and tables +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")] +internal sealed class LayoutDemo : IDemoTab +{ + private readonly List tableData = []; + private bool showTableHeaders = true; + private bool showTableBorders = true; + private readonly Random random = new(); + + private sealed record DemoItem(int Id, string Name, string Category, float Value, bool Active); + + public string TabName => "Layout & Tables"; + + public LayoutDemo() + { + // Initialize table data + for (int i = 0; i < 20; i++) + { + string[] categories = ["Category A", "Category B", "Category C"]; + tableData.Add(new DemoItem( + i, + $"Item {i + 1}", + categories[i % 3], + (float)(random.NextDouble() * 100), + random.NextDouble() > 0.5 + )); + } + } + + public void Update(float deltaTime) + { + // No updates needed for layout demo + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + // Columns + ImGui.SeparatorText("Columns Layout:"); + ImGui.Columns(3, "DemoColumns"); + ImGui.Text("Column 1"); + ImGui.NextColumn(); + ImGui.Text("Column 2"); + ImGui.NextColumn(); + ImGui.Text("Column 3"); + ImGui.NextColumn(); + + for (int i = 0; i < 9; i++) + { + ImGui.Text($"Item {i + 1}"); + ImGui.NextColumn(); + } + + ImGui.Columns(1); + + // Tables + ImGui.SeparatorText("Advanced Tables:"); + ImGui.Checkbox("Show Headers", ref showTableHeaders); + ImGui.SameLine(); + ImGui.Checkbox("Show Borders", ref showTableBorders); + + ImGuiTableFlags tableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.Resizable; + if (showTableHeaders) + { + tableFlags |= ImGuiTableFlags.RowBg; + } + if (showTableBorders) + { + tableFlags |= ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersV; + } + + if (ImGui.BeginTable("DemoTable", 5, tableFlags)) + { + if (showTableHeaders) + { + // Test flags without width parameters + ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.DefaultSort); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None); + ImGui.TableSetupColumn("Category", ImGuiTableColumnFlags.None); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.None); + ImGui.TableSetupColumn("Active", ImGuiTableColumnFlags.None); + ImGui.TableHeadersRow(); + } + + for (int row = 0; row < Math.Min(tableData.Count, 10); row++) + { + DemoItem item = tableData[row]; + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + ImGui.Text(item.Id.ToString()); + + ImGui.TableSetColumnIndex(1); + ImGui.Text(item.Name); + + ImGui.TableSetColumnIndex(2); + ImGui.Text(item.Category); + + ImGui.TableSetColumnIndex(3); + ImGui.Text($"{item.Value:F2}"); + + ImGui.TableSetColumnIndex(4); + ImGui.Text(item.Active ? "✓" : "✗"); + } + + ImGui.EndTable(); + } + + // Child windows + ImGui.SeparatorText("Child Windows:"); + if (ImGui.BeginChild("ScrollableChild", new Vector2(0, 150))) + { + for (int i = 0; i < 50; i++) + { + ImGui.Text($"Scrollable line {i + 1}"); + } + } + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/NerdFontDemo.cs b/ImGuiAppDemo/Demos/NerdFontDemo.cs new file mode 100644 index 0000000..ef43465 --- /dev/null +++ b/ImGuiAppDemo/Demos/NerdFontDemo.cs @@ -0,0 +1,84 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using Hexa.NET.ImGui; + +/// +/// Demo for Nerd Font icon support +/// +internal sealed class NerdFontDemo : IDemoTab +{ + public string TabName => "Nerd Fonts"; + + public void Update(float deltaTime) + { + // No updates needed for Nerd Font demo + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.TextWrapped("Nerd Font Icons (Patched Fonts)"); + ImGui.TextWrapped("This tab demonstrates Nerd Font icons if you're using a Nerd Font (like JetBrains Mono Nerd Font, Fira Code Nerd Font, etc.)."); + + // Powerline symbols + ImGui.SeparatorText("Powerline Symbols:"); + ImGui.Text("Basic: \uE0A0 \uE0A1 \uE0A2 \uE0B0 \uE0B1 \uE0B2 \uE0B3"); + ImGui.Text("Extra: \uE0A3 \uE0B4 \uE0B5 \uE0B6 \uE0B7 \uE0B8 \uE0CA \uE0CC \uE0CD \uE0D0 \uE0D1 \uE0D4"); + + // Font Awesome icons + ImGui.SeparatorText("Font Awesome Icons"); + ImGui.Text("Files & Folders: \uF07B \uF07C \uF15B \uF15C \uF016 \uF017 \uF019 \uF01A \uF093 \uF095"); + ImGui.Text("Git & Version Control: \uF1D3 \uF1D2 \uF126 \uF127 \uF128 \uF129 \uF12A \uF12B"); + ImGui.Text("Media & UI: \uF04B \uF04C \uF04D \uF050 \uF051 \uF048 \uF049 \uF067 \uF068 \uF00C \uF00D"); + + // Material Design icons + ImGui.SeparatorText("Material Design Icons"); + ImGui.Text("Navigation: \uF52A \uF52B \uF544 \uF53F \uF540 \uF541 \uF542 \uF543"); + ImGui.Text("Actions: \uF8D5 \uF8D6 \uF8D7 \uF8D8 \uF8D9 \uF8DA \uF8DB \uF8DC"); + ImGui.Text("Content: \uF1C1 \uF1C2 \uF1C3 \uF1C4 \uF1C5 \uF1C6 \uF1C7 \uF1C8"); + + // Weather icons + ImGui.SeparatorText("Weather Icons"); + ImGui.Text("Basic Weather: \uE30D \uE30E \uE30F \uE310 \uE311 \uE312 \uE313 \uE314"); + ImGui.Text("Temperature: \uE315 \uE316 \uE317 \uE318 \uE319 \uE31A \uE31B \uE31C"); + ImGui.Text("Wind & Pressure: \uE31D \uE31E \uE31F \uE320 \uE321 \uE322 \uE323 \uE324"); + + // Devicons + ImGui.SeparatorText("Developer Icons (Devicons)"); + ImGui.Text("Languages: \uE73C \uE73D \uE73E \uE73F \uE740 \uE741 \uE742 \uE743"); // Various programming languages + ImGui.Text("Frameworks: \uE744 \uE745 \uE746 \uE747 \uE748 \uE749 \uE74A \uE74B"); + ImGui.Text("Tools: \uE74C \uE74D \uE74E \uE74F \uE750 \uE751 \uE752 \uE753"); + + // Octicons + ImGui.SeparatorText("Octicons (GitHub Icons)"); + ImGui.Text("Version Control: \uF418 \uF419 \uF41A \uF41B \uF41C \uF41D \uF41E \uF41F"); + ImGui.Text("Issues & PRs: \uF420 \uF421 \uF422 \uF423 \uF424 \uF425 \uF426 \uF427"); + ImGui.Text("Social: \u2665 \u26A1 \uF428 \uF429 \uF42A \uF42B \uF42C \uF42D"); + + // Font Logos + ImGui.SeparatorText("Brand Logos (Font Logos)"); + ImGui.Text("Tech Brands: \uF300 \uF301 \uF302 \uF303 \uF304 \uF305 \uF306 \uF307"); + ImGui.Text("More Logos: \uF308 \uF309 \uF30A \uF30B \uF30C \uF30D \uF30E \uF30F"); + + // Pomicons + ImGui.SeparatorText("Pomicons"); + ImGui.Text("Small Icons: \uE000 \uE001 \uE002 \uE003 \uE004 \uE005 \uE006 \uE007"); + ImGui.Text("More Icons: \uE008 \uE009 \uE00A \uE00B \uE00C \uE00D"); + + ImGui.Separator(); + ImGui.TextWrapped("Note: These icons will only display correctly if you're using a Nerd Font. " + + "If you see question marks or boxes, switch to a Nerd Font like 'JetBrains Mono Nerd Font' or 'Fira Code Nerd Font'."); + + ImGui.Separator(); + ImGui.TextWrapped("Popular Nerd Fonts: JetBrains Mono Nerd Font, Fira Code Nerd Font, Hack Nerd Font, " + + "Source Code Pro Nerd Font, DejaVu Sans Mono Nerd Font, and many more at nerdfonts.com"); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/UnicodeDemo.cs b/ImGuiAppDemo/Demos/UnicodeDemo.cs new file mode 100644 index 0000000..07f4216 --- /dev/null +++ b/ImGuiAppDemo/Demos/UnicodeDemo.cs @@ -0,0 +1,64 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using Hexa.NET.ImGui; + +/// +/// Demo for Unicode and emoji support +/// +internal sealed class UnicodeDemo : IDemoTab +{ + public string TabName => "Unicode & Emojis"; + + public void Update(float deltaTime) + { + // No updates needed for Unicode demo + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + ImGui.TextWrapped("Unicode and Emoji Support (Enabled by Default)"); + ImGui.TextWrapped("ImGuiApp automatically includes support for Unicode characters and emojis. This feature works with your configured fonts."); + + ImGui.SeparatorText("Basic ASCII:"); + ImGui.Text("Hello World!"); + ImGui.Text("Accented characters: cafÊ, naïve, rÊsumÊ"); + ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ â‰Ĩ Âą × Ãˇ ∂ ∑ ∏ √ âˆĢ"); + ImGui.Text("Currency symbols: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ"); + ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ⇐ ⇒ ⇑ ⇓"); + ImGui.Text("Geometric shapes: ■ □ ▲ â–ŗ ● ○ ◆ ◇ ★ ☆"); + ImGui.Text("Miscellaneous symbols: ♠ â™Ŗ â™Ĩ â™Ļ ☀ ☁ ☂ ☃ â™Ē â™Ģ"); + + ImGui.SeparatorText("Full Emoji Range Support (if font supports them)"); + ImGui.Text("Faces: 😀 😃 😄 😁 😆 😅 😂 đŸ¤Ŗ 😊 😇 😍 😎 🤓 🧐 🤔 😴"); + ImGui.Text("Gestures: 👍 👎 👌 âœŒī¸ 🤞 🤟 🤘 🤙 👈 👉 👆 👇 â˜ī¸ ✋ 🤚 🖐"); + ImGui.Text("Objects: 🚀 đŸ’ģ 📱 🎸 🎨 🏆 🌟 💎 ⚡ đŸ”Ĩ 💡 🔧 âš™ī¸ 🔑 💰"); + ImGui.Text("Nature: 🌈 🌞 🌙 ⭐ 🌍 🌊 đŸŒŗ 🌸 đŸĻ‹ 🐝 đŸļ 🐱 đŸĻŠ đŸģ đŸŧ"); + ImGui.Text("Food: 🍎 🍌 🍕 🍔 🍟 đŸĻ 🎂 ☕ đŸē 🍷 🍓 đŸĨ‘ đŸĨ¨ 🧀 đŸ¯"); + ImGui.Text("Transport: 🚗 🚂 âœˆī¸ 🚲 đŸšĸ 🚁 🚌 đŸī¸ 🛸 🚜 đŸŽī¸ 🚙 🚕 🚐"); + ImGui.Text("Activities: âšŊ 🏀 🏈 ⚾ 🎾 🏐 🏉 🎱 🏓 🏸 đŸĨŠ â›ŗ đŸŽ¯ đŸŽĒ"); + ImGui.Text("Weather: â˜€ī¸ ⛅ â˜ī¸ đŸŒ¤ī¸ â›ˆī¸ đŸŒ§ī¸ â„ī¸ â˜ƒī¸ ⛄ đŸŒŦī¸ 💨 🌊 💧"); + ImGui.Text("Symbols: â¤ī¸ 💚 💙 💜 🖤 💛 💔 âŖī¸ 💕 💖 💗 💘 💝 ✨"); + ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙ â¤´ī¸ â¤ĩī¸ 🔀 🔁 🔂 🔄 🔃"); + ImGui.Text("Math: Âą × Ãˇ = ≠ ≈ ≤ â‰Ĩ ∞ √ ∑ ∏ ∂ âˆĢ Ί Ī€ Îą β Îŗ δ"); + ImGui.Text("Geometric: ■ □ ▲ â–ŗ ● ○ ◆ ◇ ★ ☆ ♠ â™Ŗ â™Ĩ â™Ļ â–Ē â–Ģ ◾ â—Ŋ"); + ImGui.Text("Currency: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ Âĸ â‚Ŋ ₩ ₡ â‚Ē â‚Ģ ₱ ₴ â‚Ļ ₨ â‚ĩ"); + ImGui.Text("Dingbats: ✂ ✈ ☎ ⌚ ⏰ âŗ ⌛ ⚡ ☔ ☂ ☀ ⭐ ☁ ⛅ ❄"); + ImGui.Text("Enclosed: ① ② â‘ĸ â‘Ŗ ⑤ â‘Ĩ â‘Ļ ⑧ ⑨ ⑩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ"); + + ImGui.Separator(); + ImGui.TextWrapped("Note: Character display depends on your configured font's Unicode support. " + + "If characters show as question marks, your font may not include those glyphs."); + + ImGui.Separator(); + ImGui.TextWrapped("To disable Unicode support (ASCII only), set EnableUnicodeSupport = false in your ImGuiAppConfig."); + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/Demos/UtilityDemo.cs b/ImGuiAppDemo/Demos/UtilityDemo.cs new file mode 100644 index 0000000..9f90c74 --- /dev/null +++ b/ImGuiAppDemo/Demos/UtilityDemo.cs @@ -0,0 +1,92 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo.Demos; + +using System.Text; +using Hexa.NET.ImGui; +using ktsu.ImGuiApp; + +/// +/// Demo for utility tools and debugging features +/// +internal sealed class UtilityDemo : IDemoTab +{ + private string filePath = ""; + private string fileContent = ""; + + public string TabName => "Utilities & Tools"; + + public void Update(float deltaTime) + { + // No additional windows managed here - all tools are now centralized in ImGuiApp + } + + public void Render() + { + if (ImGui.BeginTabItem(TabName)) + { + // File operations + ImGui.SeparatorText("File Operations:"); + ImGui.InputText("File Path", ref filePath, 256); + ImGui.SameLine(); + if (ImGui.Button("Load") && !string.IsNullOrEmpty(filePath)) + { + try + { + fileContent = File.ReadAllText(filePath); + } + catch (Exception ex) when (ex is FileNotFoundException or UnauthorizedAccessException) + { + // Handle file read errors gracefully + fileContent = $"Error loading file: {ex.Message}"; + } + } + + if (!string.IsNullOrEmpty(fileContent)) + { + ImGui.SeparatorText("File Content Preview:"); + ImGui.TextWrapped(fileContent.Length > 500 ? fileContent[..500] + "..." : fileContent); + } + + // System information + ImGui.SeparatorText("System Information:"); + unsafe + { + byte* ptr = ImGui.GetVersion(); + int length = 0; + while (ptr[length] != 0) + { + length++; + } + ImGui.Text($"ImGui Version: {Encoding.UTF8.GetString(ptr, length)}"); + } + ImGui.Text($"Display Size: {ImGui.GetIO().DisplaySize}"); + + // Debugging tools + ImGui.SeparatorText("Debug Tools:"); + if (ImGui.Button("Show ImGui Demo")) + { + ImGuiApp.ShowImGuiDemo(); + } + ImGui.SameLine(); + if (ImGui.Button("Show Style Editor")) + { + ImGuiApp.ShowImGuiStyleEditor(); + } + ImGui.SameLine(); + if (ImGui.Button("Show Metrics")) + { + ImGuiApp.ShowImGuiMetrics(); + } + ImGui.SameLine(); + if (ImGui.Button("Show Performance Monitor")) + { + ImGuiApp.ShowPerformanceMonitor(); + } + + ImGui.EndTabItem(); + } + } +} diff --git a/ImGuiAppDemo/ImGuiAppDemo.cs b/ImGuiAppDemo/ImGuiAppDemo.cs new file mode 100644 index 0000000..7da390c --- /dev/null +++ b/ImGuiAppDemo/ImGuiAppDemo.cs @@ -0,0 +1,126 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.ImGuiApp.Demo; + +using Hexa.NET.ImGui; +using ktsu.Extensions; +using ktsu.ImGuiApp; +using ktsu.ImGuiApp.Demo.Demos; +using ktsu.Semantics; + +internal static class ImGuiAppDemo +{ + private static bool showAbout; + private static readonly List demoTabs = []; + + static ImGuiAppDemo() + { + // Initialize all demo tabs + demoTabs.Add(new BasicWidgetsDemo()); + demoTabs.Add(new AdvancedWidgetsDemo()); + demoTabs.Add(new LayoutDemo()); + demoTabs.Add(new GraphicsDemo()); + demoTabs.Add(new DataVisualizationDemo()); + demoTabs.Add(new InputHandlingDemo()); + demoTabs.Add(new AnimationDemo()); + demoTabs.Add(new UnicodeDemo()); + demoTabs.Add(new NerdFontDemo()); + demoTabs.Add(new ImGuizmoDemo()); + demoTabs.Add(new ImNodesDemo()); + demoTabs.Add(new ImPlotDemo()); + demoTabs.Add(new UtilityDemo()); + } + + private static void Main() => ImGuiApp.Start(new() + { + Title = "ImGuiApp Demo", + IconPath = AppContext.BaseDirectory.As() / "icon.png".As(), + OnRender = OnRender, + OnAppMenu = OnAppMenu, + SaveIniSettings = false, + // Note: EnableUnicodeSupport = true by default, so Unicode and emojis are automatically enabled! + Fonts = new Dictionary + { + { nameof(ktsu.ImGuiAppDemo.Properties.Resources.CARDCHAR), ktsu.ImGuiAppDemo.Properties.Resources.CARDCHAR } + }, + // Example of configuring performance settings for throttled rendering + // Uses PID controller for accurate frame rate limiting instead of simple sleep-based approach + // VSync is disabled to allow frame limiting below monitor refresh rate + // Defaults: Kp=1.8, Ki=0.048, Kd=0.237 (from comprehensive auto-tuning) + PerformanceSettings = new() + { + EnableThrottledRendering = true, + // Using default values: Focused=30, Unfocused=5, Idle=10 FPS + // But with a shorter idle timeout for demo purposes + IdleTimeoutSeconds = 5.0, // Consider idle after 5 seconds (default is 30) + }, + }); + + private static void OnRender(float dt) + { + // Update all demo tabs + foreach (IDemoTab demo in demoTabs) + { + demo.Update(dt); + } + + // Render main demo window + RenderMainDemoWindow(); + + // Show about window if requested + if (showAbout) + { + RenderAboutWindow(); + } + } + + private static void RenderMainDemoWindow() + { + // Create tabs for different demo sections + if (ImGui.BeginTabBar("DemoTabs", ImGuiTabBarFlags.None)) + { + // Render all demo tabs + foreach (IDemoTab demo in demoTabs) + { + demo.Render(); + } + ImGui.EndTabBar(); + } + } + + private static void RenderAboutWindow() + { + ImGui.Begin("About ImGuiApp Demo", ref showAbout); + ImGui.SeparatorText("ImGuiApp Demo Application"); + ImGui.Text("This demo showcases extensive ImGui.NET features including:"); + ImGui.BulletText("Basic and advanced widgets"); + ImGui.BulletText("Layout systems (columns, tables, tabs)"); + ImGui.BulletText("Custom graphics and drawing"); + ImGui.BulletText("Data visualization and plotting"); + ImGui.BulletText("Input handling and interaction"); + ImGui.BulletText("Animations and effects"); + ImGui.BulletText("File operations and utilities"); + ImGui.BulletText("3D manipulation gizmos (ImGuizmo)"); + ImGui.BulletText("Node-based editing (ImNodes)"); + ImGui.BulletText("Advanced plotting (ImPlot)"); + ImGui.SeparatorText("Built with"); + ImGui.BulletText("Hexa.NET.ImGui"); + ImGui.BulletText("Hexa.NET.ImGuizmo - 3D manipulation gizmos"); + ImGui.BulletText("Hexa.NET.ImNodes - Node editor system"); + ImGui.BulletText("Hexa.NET.ImPlot - Advanced plotting library"); + ImGui.BulletText("Silk.NET"); + ImGui.BulletText("ktsu.ImGuiApp Framework"); + ImGui.End(); + } + + private static void OnAppMenu() + { + if (ImGui.BeginMenu("Help")) + { + ImGui.MenuItem("About", string.Empty, ref showAbout); + ImGui.EndMenu(); + } + } +} diff --git a/examples/ImGuiAppDemo/ImGuiAppDemo.csproj b/ImGuiAppDemo/ImGuiAppDemo.csproj similarity index 62% rename from examples/ImGuiAppDemo/ImGuiAppDemo.csproj rename to ImGuiAppDemo/ImGuiAppDemo.csproj index 04e5f5d..b4f90a7 100644 --- a/examples/ImGuiAppDemo/ImGuiAppDemo.csproj +++ b/ImGuiAppDemo/ImGuiAppDemo.csproj @@ -1,11 +1,5 @@ - - - - - + - net9.0 - true @@ -13,12 +7,16 @@ Always + + + + + + + - - - - + diff --git a/examples/ImGuiAppDemo/Properties/Resources.Designer.cs b/ImGuiAppDemo/Properties/Resources.Designer.cs similarity index 90% rename from examples/ImGuiAppDemo/Properties/Resources.Designer.cs rename to ImGuiAppDemo/Properties/Resources.Designer.cs index 8a95982..de06b2a 100644 --- a/examples/ImGuiAppDemo/Properties/Resources.Designer.cs +++ b/ImGuiAppDemo/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +īģŋ//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -8,10 +8,10 @@ // //------------------------------------------------------------------------------ -namespace ktsu.ImGui.Examples.App.Properties { +namespace ktsu.ImGuiAppDemo.Properties { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,15 +23,15 @@ namespace ktsu.ImGui.Examples.App.Properties { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -39,13 +39,13 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ktsu.examples.ImGuiAppDemo.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ktsu.ImGuiAppDemo.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -59,7 +59,7 @@ internal Resources() { resourceCulture = value; } } - + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/examples/ImGuiAppDemo/Properties/Resources.resx b/ImGuiAppDemo/Properties/Resources.resx similarity index 93% rename from examples/ImGuiAppDemo/Properties/Resources.resx rename to ImGuiAppDemo/Properties/Resources.resx index b4c27e0..db44261 100644 --- a/examples/ImGuiAppDemo/Properties/Resources.resx +++ b/ImGuiAppDemo/Properties/Resources.resx @@ -1,17 +1,17 @@ īģŋ - @@ -121,4 +121,4 @@ ..\res\CARDCHAR.TTF;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + \ No newline at end of file diff --git a/examples/ImGuiAppDemo/Properties/launchSettings.json b/ImGuiAppDemo/Properties/launchSettings.json similarity index 98% rename from examples/ImGuiAppDemo/Properties/launchSettings.json rename to ImGuiAppDemo/Properties/launchSettings.json index c8b4046..67ff4ad 100644 --- a/examples/ImGuiAppDemo/Properties/launchSettings.json +++ b/ImGuiAppDemo/Properties/launchSettings.json @@ -5,4 +5,4 @@ "nativeDebugging": true } } -} +} \ No newline at end of file diff --git a/examples/ImGuiAppDemo/icon.png b/ImGuiAppDemo/icon.png similarity index 100% rename from examples/ImGuiAppDemo/icon.png rename to ImGuiAppDemo/icon.png diff --git a/examples/ImGuiAppDemo/res/CARDCHAR.TTF b/ImGuiAppDemo/res/CARDCHAR.TTF similarity index 100% rename from examples/ImGuiAppDemo/res/CARDCHAR.TTF rename to ImGuiAppDemo/res/CARDCHAR.TTF diff --git a/examples/ImGuiAppDemo/res/CARDCHAR.TXT b/ImGuiAppDemo/res/CARDCHAR.TXT similarity index 100% rename from examples/ImGuiAppDemo/res/CARDCHAR.TXT rename to ImGuiAppDemo/res/CARDCHAR.TXT diff --git a/LATEST_CHANGELOG.md b/LATEST_CHANGELOG.md index 09937bf..0a9c0b8 100644 --- a/LATEST_CHANGELOG.md +++ b/LATEST_CHANGELOG.md @@ -1,3 +1,3 @@ -## v2.1.9 +## v2.1.3 -Changes since v2.1.9: +No significant changes detected since v2.1.3. diff --git a/README.md b/README.md index bd54f64..b32f12a 100644 --- a/README.md +++ b/README.md @@ -1,201 +1,337 @@ -# ktsu ImGui Suite 🎨đŸ–Ĩī¸ +# ktsu.ImGuiApp -[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE.md) -[![.NET 9](https://img.shields.io/badge/.NET-9.0-blue.svg)](https://dotnet.microsoft.com/download/dotnet/9.0) +> A .NET library that provides application scaffolding for Dear ImGui, using Silk.NET and Hexa.NET.ImGui. -A comprehensive collection of .NET libraries for building modern, beautiful, and feature-rich applications with [Dear ImGui](https://github.com/ocornut/imgui). This suite provides everything you need from application scaffolding to advanced UI components and styling systems. +[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiApp.svg)](https://www.nuget.org/packages/ktsu.ImGuiApp/) +[![License](https://img.shields.io/github/license/ktsu-dev/ImGuiApp.svg)](LICENSE.md) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.ImGuiApp.svg)](https://www.nuget.org/packages/ktsu.ImGuiApp/) +[![GitHub Stars](https://img.shields.io/github/stars/ktsu-dev/ImGuiApp?style=social)](https://github.com/ktsu-dev/ImGuiApp/stargazers) -## đŸ“Ļ Libraries Overview +## Introduction -### đŸ–Ĩī¸ [ImGui.App](ImGui.App/README.md) - Application Foundation -[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiApp.svg)](https://www.nuget.org/packages/ktsu.ImGuiApp/) +ImGuiApp is a .NET library that provides application scaffolding for [Dear ImGui](https://github.com/ocornut/imgui), using [Silk.NET](https://github.com/dotnet/Silk.NET) for OpenGL and window management and [Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui) for the ImGui bindings. It simplifies the creation of ImGui-based applications by abstracting away the complexities of window management, rendering, and input handling. -**Complete application scaffolding for Dear ImGui applications** +## Features -- **Simple API**: Create ImGui applications with minimal boilerplate -- **Advanced Performance**: PID-controlled frame limiting with auto-tuning -- **Font Management**: Unicode/emoji support with dynamic scaling -- **Texture System**: Built-in texture management with caching -- **DPI Awareness**: Full high-DPI display support -- **Debug Tools**: Comprehensive logging and performance monitoring +- **Simple API**: Create ImGui applications with minimal boilerplate code +- **Full Integration**: Seamless integration with Silk.NET for OpenGL and input handling +- **Window Management**: Automatic window state, rendering, and input handling with startup state validation +- **Performance Optimization**: Sleep-based throttled rendering with lowest-selection logic when unfocused, idle, or not visible to maximize resource savings +- **PID Frame Limiting**: Precision frame rate control using a PID controller with comprehensive auto-tuning capabilities for highly accurate target FPS achievement +- **DPI Awareness**: Built-in support for high-DPI displays and scaling +- **Font Management**: Flexible font loading system with customization options and dynamic scaling +- **Unicode & Emoji Support**: Built-in support for Unicode characters and emojis (enabled by default) +- **Texture Support**: Built-in texture management with caching and automatic cleanup for ImGui +- **Debug Logging**: Comprehensive debug logging system for troubleshooting crashes and performance issues +- **Context Handling**: Automatic OpenGL context change detection and texture reloading +- **Lifecycle Callbacks**: Customizable delegate callbacks for application events +- **Menu System**: Easy-to-use API for creating application menus +- **Positioning Guards**: Offscreen positioning checks to keep windows visible +- **Rendering Precision**: Enhanced ImGui rendering with pixel-perfect alignment and sub-pixel precision controls +- **Error Recovery**: Built-in error recovery features with configurable debugging options +- **Modern .NET**: Supports .NET 9 and newer +- **Active Development**: Open-source and actively maintained -```csharp -ImGuiApp.Start(new ImGuiAppConfig() -{ - Title = "My Application", - OnRender = delta => { ImGui.Text("Hello, ImGui!"); } -}); +## Getting Started + +### Prerequisites + +- .NET 9.0 or later (currently using .NET 9.0.301) + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.ImGuiApp +``` + +### .NET CLI + +```bash +dotnet add package ktsu.ImGuiApp ``` -### 🧩 [ImGui.Widgets](ImGui.Widgets/README.md) - Custom UI Components -[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiWidgets.svg)](https://www.nuget.org/packages/ktsu.ImGuiWidgets/) +### Package Reference -**Rich collection of custom widgets and layout tools** +```xml + +``` -- **Advanced Controls**: Knobs, SearchBox, TabPanel with drag-and-drop -- **Layout Systems**: Resizable dividers, flexible Grid, Tree views -- **Interactive Elements**: Icons with events, Color indicators -- **Utilities**: Scoped IDs, alignment helpers, text formatting +## Usage Examples + +### Basic Application + +Create a new class and call `ImGuiApp.Start()` with your application config: ```csharp -// Tabbed interface with closable, reorderable tabs -var tabPanel = new TabPanel("MyTabs", closable: true, reorderable: true); -tabPanel.AddTab("tab1", "First Tab", () => ImGui.Text("Content 1")); +using ktsu.ImGuiApp; +using Hexa.NET.ImGui; -// Powerful search with multiple filter types -ImGuiWidgets.SearchBox("##Search", ref searchTerm, ref filterType, ref matchOptions); +static class Program +{ + static void Main() + { + ImGuiApp.Start(new ImGuiAppConfig() + { + Title = "ImGuiApp Demo", + OnStart = () => { /* Initialization code */ }, + OnUpdate = delta => { /* Logic updates */ }, + OnRender = delta => { ImGui.Text("Hello, ImGuiApp!"); }, + OnAppMenu = () => + { + if (ImGui.BeginMenu("File")) + { + // Menu items + if (ImGui.MenuItem("Exit")) + { + ImGuiApp.Stop(); + } + ImGui.EndMenu(); + } + } + }); + } +} ``` -### đŸĒŸ [ImGui.Popups](ImGui.Popups/README.md) - Modal Dialogs & Popups -[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiPopups.svg)](https://www.nuget.org/packages/ktsu.ImGuiPopups/) +### Custom Font Management -**Professional modal dialogs and popup components** - -- **Input Components**: String, Int, Float inputs with validation -- **File Management**: Advanced filesystem browser with filtering -- **Selection Tools**: Searchable lists with type-safe generics -- **User Interaction**: Message dialogs, prompts, custom modals +Use the resource designer to add font files to your project, then load the fonts: ```csharp -// Get user input with validation -var inputString = new ImGuiPopups.InputString(); -inputString.Open("Enter Name", "Name:", "Default", result => ProcessName(result)); - -// File browser with pattern filtering -var browser = new ImGuiPopups.FilesystemBrowser(); -browser.Open("Open File", FilesystemBrowserMode.Open, - FilesystemBrowserTarget.File, startPath, OpenFile, new[] { "*.txt", "*.md" }); +ImGuiApp.Start(new() +{ + Title = "ImGuiApp Demo", + OnRender = OnRender, + Fonts = new Dictionary + { + { nameof(Resources.MY_FONT), Resources.MY_FONT } + }, +}); ``` -### 🎨 [ImGui.Styler](ImGui.Styler/README.md) - Themes & Styling -[![NuGet](https://img.shields.io/nuget/v/ktsu.ImGuiStyler.svg)](https://www.nuget.org/packages/ktsu.ImGuiStyler/) +Or load the font data manually: -**Advanced theming system with 50+ built-in themes** +```csharp +var fontData = File.ReadAllBytes("path/to/font.ttf"); +ImGuiApp.Start(new() +{ + Title = "ImGuiApp Demo", + OnRender = OnRender, + Fonts = new Dictionary + { + { "MyFont", fontData } + }, +}); +``` -- **Theme Library**: Catppuccin, Tokyo Night, Gruvbox, Dracula, and more -- **Interactive Browser**: Visual theme selection with live preview -- **Color Tools**: Hex support, accessibility-focused contrast -- **Scoped Styling**: Apply styles to specific UI sections safely +Then apply the font to ImGui using the `FontAppearance` class: ```csharp -// Apply global theme -Theme.Apply("Catppuccin.Mocha"); +private static void OnRender(float deltaTime) +{ + ImGui.Text("Hello, I am normal text!"); + + using (new FontAppearance("MyFont", 24)) + { + ImGui.Text("Hello, I am BIG fancy text!"); + } + + using (new FontAppearance(32)) + { + ImGui.Text("Hello, I am just huge text!"); + } + + using (new FontAppearance("MyFont")) + { + ImGui.Text("Hello, I am somewhat fancy!"); + } +} +``` + +### Unicode and Emoji Support -// Scoped color styling -using (new ScopedColor(ImGuiCol.Text, Color.FromHex("#ff6b6b"))) +ImGuiApp automatically includes support for Unicode characters and emojis. This feature is **enabled by default**, so you can use extended characters without any configuration: + +```csharp +private static void OnRender(float deltaTime) { - ImGui.Text("This text is red!"); + ImGui.Text("Basic ASCII: Hello World!"); + ImGui.Text("Accented characters: cafÊ, naïve, rÊsumÊ"); + ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ â‰Ĩ Âą × Ãˇ ∂ ∑"); + ImGui.Text("Currency symbols: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ"); + ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕"); + ImGui.Text("Emojis (if font supports): 😀 🚀 🌟 đŸ’ģ 🎨 🌈"); } +``` + +**Note**: Character display depends on your font's Unicode support. Most modern fonts include extended Latin characters and symbols, but emojis require specialized fonts. -// Center content automatically -using (new Alignment.Center(ImGui.CalcTextSize("Centered!"))) +To disable Unicode support (ASCII only), set `EnableUnicodeSupport = false`: + +```csharp +ImGuiApp.Start(new() { - ImGui.Text("Centered!"); + Title = "ASCII Only App", + EnableUnicodeSupport = false, // Disables Unicode support + // ... other settings +}); +``` + +### Texture Management + +Load and manage textures with the built-in texture management system: + +```csharp +private static void OnRender(float deltaTime) +{ + // Load texture from file path + var textureInfo = ImGuiApp.GetOrLoadTexture("path/to/texture.png"); + + // Use the texture in ImGui (using the new TextureRef API for Hexa.NET.ImGui) + ImGui.Image(textureInfo.TextureRef, new Vector2(128, 128)); + + // Clean up when done (optional - textures are cached and managed automatically) + ImGuiApp.DeleteTexture(textureInfo); } ``` -## 🚀 Quick Start +### PID Frame Limiting -### Installation +ImGuiApp features a sophisticated **PID (Proportional-Integral-Derivative) controller** for precise frame rate limiting. This system provides highly accurate target FPS control that learns and adapts to your system's characteristics. -Add the libraries you need via NuGet Package Manager or CLI: +#### Key Features -```bash -# Complete application foundation -dotnet add package ktsu.ImGuiApp +- **High-Precision Timing**: Hybrid sleep system combining `Thread.Sleep()` for coarse delays with spin-waiting for sub-millisecond accuracy +- **PID Controller**: Advanced control algorithm that learns from frame timing errors and dynamically adjusts sleep times +- **Comprehensive Auto-Tuning**: Multi-phase tuning procedure that automatically finds optimal PID parameters for your system +- **VSync Independence**: Works independently of monitor refresh rates for any target FPS +- **Real-Time Diagnostics**: Built-in performance monitoring and tuning visualization + +#### Optimized Defaults + +ImGuiApp comes pre-configured with optimal PID parameters derived from comprehensive auto-tuning: + +- **Kp: 1.800** - Proportional gain for current error response +- **Ki: 0.048** - Integral gain for accumulated error correction +- **Kd: 0.237** - Derivative gain for predictive adjustment -# Custom widgets and controls -dotnet add package ktsu.ImGuiWidgets +These defaults provide excellent frame timing accuracy out-of-the-box for most systems. -# Modal dialogs and popups -dotnet add package ktsu.ImGuiPopups +#### Configuration -# Theming and styling system -dotnet add package ktsu.ImGuiStyler +Configure frame limiting through `ImGuiAppPerformanceSettings`: + +```csharp +ImGuiApp.Start(new ImGuiAppConfig +{ + Title = "PID Frame Limited App", + OnRender = OnRender, + PerformanceSettings = new ImGuiAppPerformanceSettings + { + EnableThrottledRendering = true, + FocusedFps = 30.0, // Target 30 FPS when focused + UnfocusedFps = 5.0, // Target 5 FPS when unfocused + IdleFps = 10.0, // Target 10 FPS when idle + NotVisibleFps = 2.0, // Target 2 FPS when minimized + EnableIdleDetection = true, + IdleTimeoutSeconds = 30.0 // Idle after 30 seconds + } +}); ``` -### Basic Application +#### Auto-Tuning Procedure + +For maximum accuracy, ImGuiApp includes a comprehensive **3-phase auto-tuning system**: -Here's a complete example using multiple libraries together: +1. **Coarse Phase** (8s per test): Tests 24 parameter combinations to find the general optimal range +2. **Fine Phase** (12s per test): Tests 25 refined parameters around the best coarse result +3. **Precision Phase** (15s per test): Final optimization with 9 precision-focused parameters + +**Total tuning time**: ~12-15 minutes for maximum accuracy + +Access auto-tuning through the **Debug > Show Performance Monitor** menu, which provides: +- Real-time tuning progress visualization +- Performance metrics (Average Error, Max Error, Stability, Score) +- Interactive tuning controls and results display +- Live FPS graphs showing PID controller performance + +#### Technical Details + +The PID controller works by: +- **Measuring** actual frame times vs. target frame times +- **Calculating** error using smoothed measurements to reduce noise +- **Adjusting** sleep duration using PID mathematics: `output = Kp×error + Ki×âˆĢerror + Kd×Δerror` +- **Learning** from past performance to minimize future timing errors + +The system automatically: +- Disables VSync to prevent interference with custom frame limiting +- Pauses throttling during auto-tuning for accurate measurements +- Uses integral windup prevention to maintain stability +- Applies high-precision sleep for sub-millisecond timing accuracy + +### Full Application with Multiple Windows ```csharp using ktsu.ImGuiApp; -using ktsu.ImGuiStyler; -using ktsu.ImGuiPopups; -using ktsu.ImGuiWidgets; using Hexa.NET.ImGui; +using System.Numerics; class Program { - private static readonly ImGuiPopups.MessageOK messageOK = new(); - private static readonly TabPanel tabPanel = new("MainTabs", true, true); - private static string searchTerm = ""; - private static TextFilterType filterType = TextFilterType.Glob; - private static TextFilterMatchOptions matchOptions = TextFilterMatchOptions.ByWholeString; + private static bool _showDemoWindow = true; + private static bool _showCustomWindow = true; static void Main() { ImGuiApp.Start(new ImGuiAppConfig { - Title = "ImGui Suite Demo", + Title = "Advanced ImGuiApp Demo", + InitialWindowState = new ImGuiAppWindowState + { + Size = new Vector2(1280, 720), + Pos = new Vector2(100, 100) + }, OnStart = OnStart, + OnUpdate = OnUpdate, OnRender = OnRender, OnAppMenu = OnAppMenu, - PerformanceSettings = new() - { - FocusedFps = 60.0, - UnfocusedFps = 10.0 - } }); } private static void OnStart() { - // Apply a beautiful theme - Theme.Apply("Tokyo Night"); - - // Setup tabs - tabPanel.AddTab("widgets", "Widgets", RenderWidgetsTab); - tabPanel.AddTab("styling", "Styling", RenderStylingTab); + // Initialize your application state + Console.WriteLine("Application started"); } - private static void OnRender(float deltaTime) + private static void OnUpdate(float deltaTime) { - // Main tabbed interface - tabPanel.Draw(); - - // Render popups - messageOK.ShowIfOpen(); + // Update your application state + // This runs before rendering each frame } - private static void RenderWidgetsTab() + private static void OnRender(float deltaTime) { - ImGui.Text("Search Example:"); - ImGuiWidgets.SearchBox("##Search", ref searchTerm, ref filterType, ref matchOptions); - - if (ImGui.Button("Show Message")) - { - messageOK.Open("Hello!", "This is a popup message from ImGuiPopups!"); - } - } + // ImGui demo window + if (_showDemoWindow) + ImGui.ShowDemoWindow(ref _showDemoWindow); - private static void RenderStylingTab() - { - ImGui.Text("Theme Demo:"); - - if (ImGui.Button("Choose Theme")) - { - Theme.ShowThemeSelector("Select Theme"); - } - - using (new ScopedColor(ImGuiCol.Text, Color.FromHex("#ff6b6b"))) - { - ImGui.Text("This text is styled red!"); - } - - using (new Alignment.Center(ImGui.CalcTextSize("Centered Text"))) + // Custom window + if (_showCustomWindow) { - ImGui.Text("Centered Text"); + ImGui.Begin("Custom Window", ref _showCustomWindow); + + ImGui.Text($"Frame time: {deltaTime * 1000:F2} ms"); + ImGui.Text($"FPS: {1.0f / deltaTime:F1}"); + + if (ImGui.Button("Click Me")) + Console.WriteLine("Button clicked!"); + + ImGui.ColorEdit3("Background Color", ref _backgroundColor); + + ImGui.End(); } } @@ -205,117 +341,247 @@ class Program { if (ImGui.MenuItem("Exit")) ImGuiApp.Stop(); + ImGui.EndMenu(); } - - if (ImGui.BeginMenu("View")) + + if (ImGui.BeginMenu("Windows")) { - if (ImGui.MenuItem("Change Theme")) - Theme.ShowThemeSelector("Select Theme"); + ImGui.MenuItem("Demo Window", string.Empty, ref _showDemoWindow); + ImGui.MenuItem("Custom Window", string.Empty, ref _showCustomWindow); ImGui.EndMenu(); } } + + private static Vector3 _backgroundColor = new Vector3(0.45f, 0.55f, 0.60f); } ``` -## đŸŽ¯ Key Features +## API Reference + +### `ImGuiApp` Static Class + +The main entry point for creating and managing ImGui applications. + +#### Properties + +| Name | Type | Description | +|------|------|-------------| +| `WindowState` | `ImGuiAppWindowState` | Gets the current state of the application window | +| `Invoker` | `Invoker` | Gets an instance to delegate tasks to the window thread | +| `IsFocused` | `bool` | Gets whether the application window is focused | +| `IsVisible` | `bool` | Gets whether the application window is visible | +| `IsIdle` | `bool` | Gets whether the application is currently idle | +| `ScaleFactor` | `float` | Gets the current DPI scale factor | + +#### Methods + +| Name | Parameters | Return Type | Description | +|------|------------|-------------|-------------| +| `Start` | `ImGuiAppConfig config` | `void` | Starts the ImGui application with the provided configuration | +| `Stop` | | `void` | Stops the running application | +| `GetOrLoadTexture` | `AbsoluteFilePath path` | `ImGuiAppTextureInfo` | Loads a texture from file or returns cached texture info if already loaded | +| `TryGetTexture` | `AbsoluteFilePath path, out ImGuiAppTextureInfo textureInfo` | `bool` | Attempts to get a cached texture by path | +| `DeleteTexture` | `uint textureId` | `void` | Deletes a texture and frees its resources | +| `DeleteTexture` | `ImGuiAppTextureInfo textureInfo` | `void` | Deletes a texture and frees its resources (convenience overload) | +| `CleanupAllTextures` | | `void` | Cleans up all loaded textures | +| `SetWindowIcon` | `string iconPath` | `void` | Sets the window icon using the specified icon file path | +| `EmsToPx` | `float ems` | `int` | Converts a value in ems to pixels based on current font size | +| `PtsToPx` | `int pts` | `int` | Converts a value in points to pixels based on current scale factor | +| `UseImageBytes` | `Image image, Action action` | `void` | Executes an action with temporary access to image bytes using pooled memory | + +### `ImGuiAppConfig` Class + +Configuration for the ImGui application. + +#### Properties + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `TestMode` | `bool` | `false` | Whether the application is running in test mode | +| `Title` | `string` | `"ImGuiApp"` | The window title | +| `IconPath` | `string` | `""` | The file path to the application window icon | +| `InitialWindowState` | `ImGuiAppWindowState` | `new()` | The initial state of the application window | +| `Fonts` | `Dictionary` | `[]` | Font name to font data mapping | +| `EnableUnicodeSupport` | `bool` | `true` | Whether to enable Unicode and emoji support | +| `SaveIniSettings` | `bool` | `true` | Whether ImGui should save window settings to imgui.ini | +| `PerformanceSettings` | `ImGuiAppPerformanceSettings` | `new()` | Performance settings for throttled rendering | +| `OnStart` | `Action` | `() => { }` | Called when the application starts | +| `FrameWrapperFactory` | `Func` | `() => null` | Factory for creating frame wrappers | +| `OnUpdate` | `Action` | `(delta) => { }` | Called each frame before rendering (param: delta time) | +| `OnRender` | `Action` | `(delta) => { }` | Called each frame for rendering (param: delta time) | +| `OnAppMenu` | `Action` | `() => { }` | Called each frame for rendering the application menu | +| `OnMoveOrResize` | `Action` | `() => { }` | Called when the application window is moved or resized | + +### `ImGuiAppPerformanceSettings` Class + +Configuration for performance optimization and throttled rendering. Uses a sophisticated **PID controller with high-precision timing** to achieve accurate target frame rates while maintaining system resource efficiency. The system combines Thread.Sleep for coarse delays with spin-waiting for sub-millisecond precision, and automatically disables VSync to prevent interference with custom frame limiting. + +#### Properties + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `EnableThrottledRendering` | `bool` | `true` | Enables/disables throttled rendering feature | +| `FocusedFps` | `double` | `30.0` | Target frame rate when the window is focused and active | +| `UnfocusedFps` | `double` | `5.0` | Target frame rate when the window is unfocused | +| `IdleFps` | `double` | `10.0` | Target frame rate when the application is idle (no user input) | +| `NotVisibleFps` | `double` | `2.0` | Target frame rate when the window is not visible (minimized or hidden) | +| `EnableIdleDetection` | `bool` | `true` | Enables/disables idle detection based on user input | +| `IdleTimeoutSeconds` | `double` | `30.0` | Time in seconds without user input before considering the app idle | + +#### Example Usage + +```csharp +ImGuiApp.Start(new ImGuiAppConfig +{ + Title = "My Application", + OnRender = OnRender, + PerformanceSettings = new ImGuiAppPerformanceSettings + { + EnableThrottledRendering = true, + FocusedFps = 60.0, // Custom higher rate when focused + UnfocusedFps = 15.0, // Custom rate when unfocused + IdleFps = 2.0, // Custom very low rate when idle + NotVisibleFps = 1.0, // Custom ultra-low rate when minimized + EnableIdleDetection = true, + IdleTimeoutSeconds = 10.0 // Custom idle timeout + } + // PID controller uses optimized defaults: Kp=1.8, Ki=0.048, Kd=0.237 + // For fine-tuning, use Debug > Show Performance Monitor > Start Auto-Tuning +}); +``` + +This feature automatically: +- Uses a **PID controller** with optimized defaults for highly accurate frame rate targeting +- Combines **Thread.Sleep** with **spin-waiting** for sub-millisecond timing precision +- Disables **VSync** automatically to prevent interference with custom frame limiting +- Detects when the window loses/gains focus and visibility state (minimized/hidden) +- Tracks user input (keyboard, mouse movement, clicks, scrolling) for idle detection +- Evaluates all applicable throttling conditions and selects the lowest frame rate +- Saves significant CPU and GPU resources without affecting user experience +- Provides instant transitions between different performance states +- Uses conservative defaults: 30 FPS focused, 5 FPS unfocused, 10 FPS idle, 2 FPS not visible -- **đŸ–Ĩī¸ Complete Application Framework**: Everything needed for production ImGui applications -- **🎨 Professional Theming**: 50+ themes with interactive browser and accessibility features -- **🧩 Rich Widget Library**: Advanced controls like tabbed interfaces, search boxes, and knobs -- **đŸĒŸ Modal System**: Type-safe popups, file browsers, and input validation -- **⚡ High Performance**: PID-controlled frame limiting with auto-tuning capabilities -- **đŸŽ¯ Developer Friendly**: Clean APIs, comprehensive documentation, and extensive examples -- **🔧 Production Ready**: Debug logging, error handling, and resource management -- **🌐 Modern .NET**: Built for .NET 9+ with latest language features +The **PID controller** learns from timing errors and adapts to your system's characteristics, providing much more accurate frame rate control than simple sleep-based methods. The throttling system uses a "lowest wins" approach - if multiple conditions apply (e.g., unfocused + idle), the lowest frame rate is automatically selected for maximum resource savings. -## 📚 Documentation +### `FontAppearance` Class -Each library has comprehensive documentation with examples: +A utility class for applying font styles using a using statement. -- **[📖 ImGui.App Documentation](ImGui.App/README.md)** - Application scaffolding, performance tuning, font management -- **[📖 ImGui.Widgets Documentation](ImGui.Widgets/README.md)** - Widget gallery, layout systems, interactive controls -- **[📖 ImGui.Popups Documentation](ImGui.Popups/README.md)** - Modal dialogs, file browsers, input validation -- **[📖 ImGui.Styler Documentation](ImGui.Styler/README.md)** - Theme gallery, color tools, styling utilities +#### Constructors -## 🎮 Demo Applications +| Constructor | Parameters | Description | +|-------------|------------|-------------| +| `FontAppearance` | `string fontName` | Creates a font appearance with the named font at default size | +| `FontAppearance` | `float fontSize` | Creates a font appearance with the default font at the specified size | +| `FontAppearance` | `string fontName, float fontSize` | Creates a font appearance with the named font at the specified size | -The repository includes comprehensive demo applications showcasing all features: +### `ImGuiAppWindowState` Class -```bash -# Clone the repository -git clone https://github.com/ktsu-dev/ImGui.git -cd ImGui +Represents the state of the application window. -# Run the main demo (showcases all libraries) -dotnet run --project examples/ImGuiAppDemo +#### Properties -# Run individual library demos -dotnet run --project examples/ImGuiWidgetsDemo -dotnet run --project examples/ImGuiPopupsDemo -dotnet run --project examples/ImGuiStylerDemo -``` +| Name | Type | Description | +|------|------|-------------| +| `Size` | `Vector2` | The size of the window | +| `Pos` | `Vector2` | The position of the window | +| `LayoutState` | `WindowState` | The layout state of the window (Normal, Maximized, etc.) | -Each demo includes: -- **Interactive Examples**: Try all features with live code -- **Performance Testing**: See PID frame limiting and throttling in action -- **Theme Gallery**: Browse and apply all 50+ built-in themes -- **Widget Showcase**: Complete widget and layout demonstrations -- **Integration Examples**: How libraries work together +## Debug Features -## đŸ› ī¸ Requirements +ImGuiApp includes comprehensive debug logging capabilities to help troubleshoot crashes and performance issues: -- **.NET 9.0** or later -- **Windows, macOS, or Linux** (cross-platform support via Silk.NET) -- **OpenGL 3.3** or higher (handled automatically) +### Debug Logging -## 🤝 Contributing +The application automatically creates debug logs on the desktop (`ImGuiApp_Debug.log`) when issues occur. These logs include: +- Window initialization steps +- OpenGL context creation +- Font loading progress +- Error conditions and exceptions -We welcome contributions! Here's how to get started: +### Debug Menu -1. **Fork** the repository -2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) -3. **Make** your changes with tests -4. **Commit** your changes (`git commit -m 'Add amazing feature'`) -5. **Push** to the branch (`git push origin feature/amazing-feature`) -6. **Open** a Pull Request +When using the `OnAppMenu` callback, ImGuiApp automatically adds a Debug menu with options to: +- Show ImGui Demo Window +- Show ImGui Metrics Window +- Show Performance Monitor (real-time FPS graphs and throttling visualization) -### Development Setup +### Performance Monitoring -```bash -git clone https://github.com/ktsu-dev/ImGui.git -cd ImGui -dotnet restore -dotnet build -``` +The core library includes a built-in performance monitor accessible via the debug menu. It provides: +- Real-time FPS tracking and visualization +- Throttling state monitoring (focused/unfocused/idle/not visible) +- Performance testing tips and interactive guidance +- Historical performance data graphing + +Access it through: **Debug > Show Performance Monitor** + +## Demo Application + +Check out the included demo project to see a comprehensive working example: + +1. Clone or download the repository +2. Open the solution in Visual Studio (or run dotnet build) +3. Start the ImGuiAppDemo project to see a feature-rich ImGui application +4. Explore the comprehensive demo tabs: + - **Basic Widgets**: Essential UI elements like buttons, inputs, sliders + - **Advanced Widgets**: Complex controls, combo boxes, list boxes, color pickers + - **Layout & Tables**: Column layouts, advanced tables with sorting and resizing + - **Graphics & Drawing**: Custom drawing using ImDrawList for canvas-like functionality + - **Data Visualization**: Real-time plotting with performance metrics and graphs + - **Input Handling**: Mouse input information and drag-and-drop demonstrations + - **Animation**: Dynamic content and animated elements + - **Unicode & Emojis**: Extended character support with comprehensive symbol sets + - **Nerd Font Icons**: Icon sets and specialized glyph demonstrations + - **ImGuizmo**: 3D gizmo controls for object manipulation + - **ImNodes**: Node-based editor demonstrations + - **ImPlot**: Advanced plotting and charting capabilities + - **Utilities & Tools**: Modal dialogs, popups, file operations, and system information +5. Use the debug menu to access additional features: + - **Debug > Show Performance Monitor**: Real-time FPS graph showing PID controller performance with comprehensive auto-tuning capabilities + - **Debug > Show ImGui Demo**: Official ImGui demo window + - **Debug > Show ImGui Metrics**: ImGui internal metrics and debugging info + +The **Performance Monitor** includes: +- **Live FPS graphs** that visualize frame rate changes as you focus/unfocus the window, let it go idle, or minimize it +- **PID Controller diagnostics** showing real-time proportional, integral, and derivative values +- **Comprehensive Auto-Tuning** with 3-phase optimization (Coarse, Fine, Precision phases) +- **Performance metrics** including Average Error, Max Error, Stability, and composite Score +- **Interactive tuning controls** to start/stop optimization and view detailed results + +Perfect for seeing both the throttling system and PID controller work in real-time! + +The demo application has been recently enhanced with comprehensive ImGui feature demonstrations, including advanced rendering precision improvements with pixel-perfect alignment, error recovery features, and extensive widget showcases across 13 different demo tabs. + +## Contributing + +Contributions are welcome! Here's how you can help: -Please ensure: -- Code follows existing style conventions -- All tests pass (`dotnet test`) -- Documentation is updated for new features -- Examples demonstrate new functionality +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request -## 📄 License +Please make sure to update tests as appropriate and adhere to the existing coding style. -This project is licensed under the **MIT License** - see the [LICENSE.md](LICENSE.md) file for details. +## License -## 🙏 Acknowledgments +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. -- **[Dear ImGui](https://github.com/ocornut/imgui)** - The amazing immediate mode GUI library -- **[Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui)** - Excellent .NET bindings for Dear ImGui -- **[Silk.NET](https://github.com/dotnet/Silk.NET)** - Cross-platform .NET OpenGL and windowing -- **Theme Communities** - Catppuccin, Tokyo Night, Gruvbox creators and communities -- **Contributors** - Everyone who has contributed code, themes, bug reports, and feedback +## Versioning -## 🔗 Related Projects +Check the [CHANGELOG.md](CHANGELOG.md) for detailed release notes and version changes. -- **[ktsu.ThemeProvider](https://github.com/ktsu-dev/ThemeProvider)** - Semantic theming foundation -- **[ktsu.Extensions](https://github.com/ktsu-dev/Extensions)** - Utility extension methods -- **[ktsu.StrongPaths](https://github.com/ktsu-dev/StrongPaths)** - Type-safe path handling -- **[ktsu.TextFilter](https://github.com/ktsu-dev/TextFilter)** - Advanced text filtering utilities +## Acknowledgements ---- +- [Dear ImGui](https://github.com/ocornut/imgui) - The immediate mode GUI library +- [Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui) - .NET bindings for Dear ImGui +- [Silk.NET](https://github.com/dotnet/Silk.NET) - .NET bindings for OpenGL and windowing (includes OpenGL, Input, SDL2, and additional extensions) +- [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) - Cross-platform image processing library +- All contributors and the .NET community for their support -**Made with â¤ī¸ by the ktsu.dev team** +## Support -*Build beautiful, performant desktop applications with the power of Dear ImGui and .NET* +If you encounter any issues or have questions, please [open an issue](https://github.com/ktsu-dev/ImGuiApp/issues). diff --git a/VERSION.md b/VERSION.md index 63a1a1c..ac2cdeb 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -2.1.9 +2.1.3 diff --git a/examples/ImGuiAppDemo/ImGuiAppDemo.cs b/examples/ImGuiAppDemo/ImGuiAppDemo.cs deleted file mode 100644 index 31a4cfe..0000000 --- a/examples/ImGuiAppDemo/ImGuiAppDemo.cs +++ /dev/null @@ -1,966 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Examples.App; - -using System.Numerics; -using System.Text; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.App; -using ktsu.ImGui.Examples.App.Properties; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; - -internal static class ImGuiAppDemo -{ - private static bool showImGuiDemo; - private static bool showStyleEditor; - private static bool showMetrics; - private static bool showAbout; - - // Demo state - Basic Widgets - private static float sliderValue = 0.5f; - private static int counter; - private static bool checkboxState; - private static string inputText = "Type here..."; - private static Vector3 colorPickerValue = new(0.4f, 0.7f, 0.2f); - private static Vector4 color4Value = new(1.0f, 0.5f, 0.2f, 1.0f); - private static readonly Random random = new(); - private static readonly List plotValues = []; - private static float plotRefreshTime; - - // Advanced widget states - private static int comboSelection; - private static readonly string[] comboItems = ["Item 1", "Item 2", "Item 3", "Item 4"]; - private static int listboxSelection; - private static readonly string[] listboxItems = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]; - private static float dragFloat = 1.0f; - private static int dragInt = 50; - private static Vector3 dragVector = new(1.0f, 2.0f, 3.0f); - private static float angle; - - // Table demo state - private static readonly List tableData = []; - private static bool showTableHeaders = true; - private static bool showTableBorders = true; - - // Text rendering state - private static readonly StringBuilder textBuffer = new(1024); - private static bool wrapText = true; - private static float textSpeed = 50.0f; - private static float animationTime; - - // Canvas drawing state - private static readonly List canvasPoints = []; - private static Vector4 drawColor = new(1.0f, 1.0f, 0.0f, 1.0f); - private static float brushSize = 5.0f; - - // Modal and popup states - private static bool showModal; - private static bool showPopup; - private static string modalResult = ""; - - // File operations - private static string filePath = ""; - private static string fileContent = ""; - - // Animation demo - private static float bounceOffset; - private static float pulseScale = 1.0f; - - // Additional UI state - private static int radioSelection; - private static string modalInputBuffer = ""; - - private sealed record DemoItem(int Id, string Name, string Category, float Value, bool Active); - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "")] - static ImGuiAppDemo() - { - // Initialize table data - for (int i = 0; i < 20; i++) - { - string[] categories = ["Category A", "Category B", "Category C"]; - tableData.Add(new DemoItem( - i, - $"Item {i + 1}", - categories[i % 3], - (float)(random.NextDouble() * 100), - random.NextDouble() > 0.5 - )); - } - - textBuffer.Append("This is a demonstration of ImGui text editing capabilities.\n"); - textBuffer.Append("You can edit this text, and it will update in real-time.\n"); - textBuffer.Append("ImGui supports multi-line text editing with syntax highlighting possibilities."); - } - - private static void Main() => ImGuiApp.Start(new() - { - Title = "ImGuiApp Demo", - IconPath = AppContext.BaseDirectory.As() / "icon.png".As(), - OnRender = OnRender, - OnAppMenu = OnAppMenu, - SaveIniSettings = false, - // Note: EnableUnicodeSupport = true by default, so Unicode and emojis are automatically enabled! - Fonts = new Dictionary - { - { nameof(Resources.CARDCHAR), Resources.CARDCHAR } - }, - // Example of handling global scale changes for accessibility - OnGlobalScaleChanged = (scale) => Console.WriteLine($"Global UI scale changed to {scale * 100:F0}% - This can be persisted to user preferences"), - // Example of configuring performance settings for throttled rendering - // Uses PID controller for accurate frame rate limiting instead of simple sleep-based approach - // VSync is disabled to allow frame limiting below monitor refresh rate - // Defaults: Kp=1.8, Ki=0.048, Kd=0.237 (from comprehensive auto-tuning) - PerformanceSettings = new() - { - EnableThrottledRendering = true, - // Using default values: Focused=30, Unfocused=5, Idle=10 FPS - // But with a shorter idle timeout for demo purposes - IdleTimeoutSeconds = 5.0, // Consider idle after 5 seconds (default is 30) - }, - }); - - private static void OnRender(float dt) - { - UpdateAnimations(dt); - RenderMainDemoWindow(); - - // Show additional windows based on menu toggles - if (showImGuiDemo) - { - ImGui.ShowDemoWindow(ref showImGuiDemo); - } - - if (showStyleEditor) - { - ImGui.Begin("Style Editor", ref showStyleEditor); - ImGui.ShowStyleEditor(); - ImGui.End(); - } - - if (showMetrics) - { - ImGui.ShowMetricsWindow(ref showMetrics); - } - - if (showAbout) - { - RenderAboutWindow(); - } - - // Handle modals and popups - RenderModalAndPopups(); - - // Update plot data - UpdatePlotData(dt); - } - - private static void RenderMainDemoWindow() - { - // Create tabs for different demo sections - if (ImGui.BeginTabBar("DemoTabs", ImGuiTabBarFlags.None)) - { - RenderBasicWidgetsTab(); - RenderAdvancedWidgetsTab(); - RenderLayoutTab(); - RenderGraphicsTab(); - RenderDataVisualizationTab(); - RenderInputHandlingTab(); - RenderAnimationTab(); - RenderUnicodeTab(); - RenderNerdFontTab(); - RenderUtilityTab(); - ImGui.EndTabBar(); - } - } - - private static void RenderBasicWidgetsTab() - { - if (ImGui.BeginTabItem("Basic Widgets")) - { - ImGui.TextWrapped("This tab demonstrates basic ImGui widgets and controls."); - ImGui.Separator(); - - // Buttons - ImGui.Text("Buttons:"); - if (ImGui.Button("Regular Button")) - { - counter++; - } - - ImGui.SameLine(); - if (ImGui.SmallButton("Small")) - { - counter++; - } - - ImGui.SameLine(); - if (ImGui.ArrowButton("##left", ImGuiDir.Left)) - { - counter--; - } - - ImGui.SameLine(); - if (ImGui.ArrowButton("##right", ImGuiDir.Right)) - { - counter++; - } - - ImGui.SameLine(); - ImGui.Text($"Counter: {counter}"); - - // Checkboxes and Radio buttons - ImGui.Separator(); - ImGui.Text("Selection Controls:"); - ImGui.Checkbox("Checkbox", ref checkboxState); - - ImGui.RadioButton("Option 1", ref radioSelection, 0); - ImGui.SameLine(); - ImGui.RadioButton("Option 2", ref radioSelection, 1); - ImGui.SameLine(); - ImGui.RadioButton("Option 3", ref radioSelection, 2); - - // Sliders - ImGui.Separator(); - ImGui.Text("Sliders:"); - ImGui.SliderFloat("Float Slider", ref sliderValue, 0.0f, 1.0f); - ImGui.SliderFloat("Angle", ref angle, 0.0f, 360.0f, "%.1f deg"); - ImGui.SliderInt("Int Slider", ref dragInt, 0, 100); - - // Input fields - ImGui.Separator(); - ImGui.Text("Input Fields:"); - ImGui.InputText("Text Input", ref inputText, 100); - ImGui.InputFloat("Float Input", ref dragFloat); - ImGui.InputFloat3("Vector3 Input", ref dragVector); - - // Combo boxes - ImGui.Separator(); - ImGui.Text("Dropdowns:"); - ImGui.Combo("Combo Box", ref comboSelection, comboItems, comboItems.Length); - ImGui.ListBox("List Box", ref listboxSelection, listboxItems, listboxItems.Length, 4); - - ImGui.EndTabItem(); - } - } - - private static void RenderAdvancedWidgetsTab() - { - if (ImGui.BeginTabItem("Advanced Widgets")) - { - // Color controls - ImGui.Text("Color Controls:"); - ImGui.ColorEdit3("Color RGB", ref colorPickerValue); - ImGui.ColorEdit4("Color RGBA", ref color4Value); - ImGui.SetNextItemWidth(200.0f); - ImGui.ColorPicker3("Color Picker", ref colorPickerValue); - - ImGui.Separator(); - - // Tree view - ImGui.Text("Tree View:"); - if (ImGui.TreeNode("Root Node")) - { - for (int i = 0; i < 5; i++) - { - string nodeName = $"Child Node {i}"; - bool nodeOpen = ImGui.TreeNode(nodeName); - - if (i == 2 && nodeOpen) - { - for (int j = 0; j < 3; j++) - { - if (ImGui.TreeNode($"Grandchild {j}")) - { - ImGui.Text($"Leaf item {j}"); - ImGui.TreePop(); - } - } - } - else if (nodeOpen) - { - ImGui.Text($"Content of {nodeName}"); - } - - if (nodeOpen) - { - ImGui.TreePop(); - } - } - ImGui.TreePop(); - } - - ImGui.Separator(); - - // Progress bars and loading indicators - ImGui.Text("Progress Indicators:"); - float progress = ((float)Math.Sin(animationTime * 2.0) * 0.5f) + 0.5f; - ImGui.ProgressBar(progress, new Vector2(-1, 0), $"{progress * 100:F1}%"); - - // Spinner-like effect - ImGui.Text("Loading..."); - ImGui.SameLine(); - for (int i = 0; i < 8; i++) - { - float rotation = (animationTime * 5.0f) + (i * MathF.PI / 4.0f); - float alpha = (MathF.Sin(rotation) + 1.0f) * 0.5f; - ImGui.TextColored(new Vector4(1, 1, 1, alpha), "●"); - if (i < 7) - { - ImGui.SameLine(); - } - } - - ImGui.EndTabItem(); - } - } - - private static void RenderLayoutTab() - { - if (ImGui.BeginTabItem("Layout & Tables")) - { - // Columns - ImGui.Text("Columns Layout:"); - ImGui.Columns(3, "DemoColumns"); - ImGui.Separator(); - - ImGui.Text("Column 1"); - ImGui.NextColumn(); - ImGui.Text("Column 2"); - ImGui.NextColumn(); - ImGui.Text("Column 3"); - ImGui.NextColumn(); - - for (int i = 0; i < 9; i++) - { - ImGui.Text($"Item {i + 1}"); - ImGui.NextColumn(); - } - - ImGui.Columns(1); - ImGui.Separator(); - - // Tables - ImGui.Text("Advanced Tables:"); - ImGui.Checkbox("Show Headers", ref showTableHeaders); - ImGui.SameLine(); - ImGui.Checkbox("Show Borders", ref showTableBorders); - - ImGuiTableFlags tableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.Resizable; - if (showTableHeaders) - { - tableFlags |= ImGuiTableFlags.RowBg; - } - if (showTableBorders) - { - tableFlags |= ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersV; - } - - if (ImGui.BeginTable("DemoTable", 5, tableFlags)) - { - if (showTableHeaders) - { - // Test flags without width parameters - ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.DefaultSort); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None); - ImGui.TableSetupColumn("Category", ImGuiTableColumnFlags.None); - ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.None); - ImGui.TableSetupColumn("Active", ImGuiTableColumnFlags.None); - ImGui.TableHeadersRow(); - } - - for (int row = 0; row < Math.Min(tableData.Count, 10); row++) - { - DemoItem item = tableData[row]; - ImGui.TableNextRow(); - - ImGui.TableSetColumnIndex(0); - ImGui.Text(item.Id.ToString()); - - ImGui.TableSetColumnIndex(1); - ImGui.Text(item.Name); - - ImGui.TableSetColumnIndex(2); - ImGui.Text(item.Category); - - ImGui.TableSetColumnIndex(3); - ImGui.Text($"{item.Value:F2}"); - - ImGui.TableSetColumnIndex(4); - ImGui.Text(item.Active ? "✓" : "✗"); - } - - ImGui.EndTable(); - } - - ImGui.Separator(); - - // Child windows - ImGui.Text("Child Windows:"); - if (ImGui.BeginChild("ScrollableChild", new Vector2(0, 150))) - { - for (int i = 0; i < 50; i++) - { - ImGui.Text($"Scrollable line {i + 1}"); - } - } - ImGui.EndChild(); - - ImGui.EndTabItem(); - } - } - - private static void RenderGraphicsTab() - { - if (ImGui.BeginTabItem("Graphics & Drawing")) - { - // Image display - AbsoluteFilePath iconPath = AppContext.BaseDirectory.As() / "icon.png".As(); - ImGuiAppTextureInfo iconTexture = ImGuiApp.GetOrLoadTexture(iconPath); - - ImGui.Text("Image Display:"); - ImGui.Image(iconTexture.TextureRef, new Vector2(64, 64)); - ImGui.SameLine(); - ImGui.Image(iconTexture.TextureRef, new Vector2(32, 32)); - ImGui.SameLine(); - ImGui.Image(iconTexture.TextureRef, new Vector2(16, 16)); - - ImGui.Separator(); - - // Custom drawing with ImDrawList - ImGui.Text("Custom Drawing Canvas:"); - ImGui.ColorEdit4("Draw Color", ref drawColor); - ImGui.SliderFloat("Brush Size", ref brushSize, 1.0f, 20.0f); - - if (ImGui.Button("Clear Canvas")) - { - canvasPoints.Clear(); - } - - Vector2 canvasPos = ImGui.GetCursorScreenPos(); - Vector2 canvasSize = new(400, 200); - - // Draw canvas background - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - drawList.AddRectFilled(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.1f, 0.1f, 0.1f, 1.0f))); - drawList.AddRect(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0.5f, 0.5f, 1.0f))); - - // Handle mouse input for drawing - ImGui.InvisibleButton("Canvas", canvasSize); - if (ImGui.IsItemActive() && ImGui.IsMouseDown(ImGuiMouseButton.Left)) - { - Vector2 mousePos = ImGui.GetMousePos() - canvasPos; - if (mousePos.X >= 0 && mousePos.Y >= 0 && mousePos.X <= canvasSize.X && mousePos.Y <= canvasSize.Y) - { - canvasPoints.Add(mousePos); - } - } - - // Draw points - uint color = ImGui.ColorConvertFloat4ToU32(drawColor); - foreach (Vector2 point in canvasPoints) - { - drawList.AddCircleFilled(canvasPos + point, brushSize, color); - } - - // Draw some simple shapes for demonstration - ImGui.Separator(); - ImGui.Text("Shape Examples:"); - Vector2 shapeStart = ImGui.GetCursorScreenPos(); - - // Simple animated circle - float t = animationTime; - Vector2 center = shapeStart + new Vector2(100, 50); - float radius = 20 + (MathF.Sin(t * 2) * 5); - drawList.AddCircle(center, radius, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 1)), 16, 2.0f); - - // Moving rectangle - Vector2 rectPos = shapeStart + new Vector2(200 + (MathF.Sin(t) * 30), 30); - drawList.AddRectFilled(rectPos, rectPos + new Vector2(40, 40), ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 0, 0.7f))); - - ImGui.Dummy(new Vector2(400, 100)); // Reserve space - - ImGui.EndTabItem(); - } - } - - private static void RenderDataVisualizationTab() - { - if (ImGui.BeginTabItem("Data Visualization")) - { - ImGui.Text("Real-time Data Plots:"); - - // Line plot - if (plotValues.Count > 0) - { - float[] values = [.. plotValues]; - ImGui.PlotLines("Random Values", ref values[0], values.Length, 0, - $"Current: {values[^1]:F2}", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100)); - - ImGui.PlotHistogram("Distribution", ref values[0], values.Length, 0, - "Histogram", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100)); - } - - ImGui.Separator(); - - // Performance note - ImGui.Text("Performance Metrics:"); - ImGui.TextWrapped("Performance monitoring is now available in the Debug menu! Use 'Debug > Show Performance Monitor' to see real-time FPS graphs and throttling state."); - - ImGui.Separator(); - - // Font demonstrations - ImGui.Text("Custom Font Rendering:"); - using (new FontAppearance(nameof(Resources.CARDCHAR), 16)) - { - ImGui.Text("Small custom font text"); - } - - using (new FontAppearance(nameof(Resources.CARDCHAR), 24)) - { - ImGui.Text("Medium custom font text"); - } - - using (new FontAppearance(nameof(Resources.CARDCHAR), 32)) - { - ImGui.Text("Large custom font text"); - } - - // Text formatting examples - ImGui.Separator(); - ImGui.Text("Text Formatting:"); - ImGui.TextColored(new Vector4(1, 0, 0, 1), "Red text"); - ImGui.TextColored(new Vector4(0, 1, 0, 1), "Green text"); - ImGui.TextColored(new Vector4(0, 0, 1, 1), "Blue text"); - ImGui.TextWrapped("This is a long line of text that should wrap to multiple lines when the window is not wide enough to contain it all on a single line."); - - ImGui.EndTabItem(); - } - } - - private static void RenderInputHandlingTab() - { - if (ImGui.BeginTabItem("Input & Interaction")) - { - ImGui.Text("Mouse Information:"); - Vector2 mousePos = ImGui.GetMousePos(); - Vector2 mouseDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Left); - ImGui.Text($"Mouse Position: ({mousePos.X:F1}, {mousePos.Y:F1})"); - ImGui.Text($"Mouse Delta: ({mouseDelta.X:F1}, {mouseDelta.Y:F1})"); - ImGui.Text($"Left Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Left) ? "DOWN" : "UP")}"); - ImGui.Text($"Right Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Right) ? "DOWN" : "UP")}"); - - ImGui.Separator(); - - // Simple drag demonstration - ImGui.Text("Drag & Drop:"); - ImGui.Button("Drag Source", new Vector2(100, 50)); - ImGui.SameLine(); - ImGui.Button("Drop Target", new Vector2(100, 50)); - ImGui.Text("(Drag and drop functionality would require more complex implementation)"); - - ImGui.Separator(); - - // Text editing - ImGui.Text("Multi-line Text Editor:"); - ImGui.Checkbox("Word Wrap", ref wrapText); - ImGuiInputTextFlags textFlags = ImGuiInputTextFlags.AllowTabInput; - if (!wrapText) - { - textFlags |= ImGuiInputTextFlags.NoHorizontalScroll; - } - - string textContent = textBuffer.ToString(); - if (ImGui.InputTextMultiline("##TextEditor", ref textContent, 1024, new Vector2(-1, 150), textFlags)) - { - textBuffer.Clear(); - textBuffer.Append(textContent); - } - - ImGui.Separator(); - - // Popup and modal buttons - ImGui.Text("Popups and Modals:"); - if (ImGui.Button("Show Modal")) - { - showModal = true; - modalResult = ""; - } - - ImGui.SameLine(); - if (ImGui.Button("Show Popup")) - { - showPopup = true; - } - - if (!string.IsNullOrEmpty(modalResult)) - { - ImGui.Text($"Modal Result: {modalResult}"); - } - - ImGui.EndTabItem(); - } - } - - private static void RenderAnimationTab() - { - if (ImGui.BeginTabItem("Animation & Effects")) - { - ImGui.Text("Animation Examples:"); - - // Simple animations - ImGui.Text("Bouncing Animation:"); - Vector2 ballPos = ImGui.GetCursorScreenPos(); - ballPos.Y += bounceOffset; - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - drawList.AddCircleFilled(ballPos + new Vector2(50, 50), 20, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 0, 1))); - ImGui.Dummy(new Vector2(100, 100)); - - // Pulsing element - ImGui.Text("Pulse Animation:"); - Vector2 pulsePos = ImGui.GetCursorScreenPos(); - float pulseSize = 20 * pulseScale; - drawList.AddCircleFilled(pulsePos + new Vector2(50, 50), pulseSize, - ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0, 1, 0.7f))); - ImGui.Dummy(new Vector2(100, 100)); - - ImGui.Separator(); - - // Animation controls - ImGui.Text("Animation Controls:"); - ImGui.SliderFloat("Text Speed", ref textSpeed, 10.0f, 200.0f); - - // Animated text (simplified) - ImGui.Text("Animated text effects:"); - for (int i = 0; i < 20; i++) - { - float wave = (MathF.Sin((animationTime * 3.0f) + (i * 0.5f)) * 0.5f) + 0.5f; - ImGui.TextColored(new Vector4(wave, 1.0f - wave, 0.5f, 1.0f), i % 5 == 4 ? " " : "▓"); - if (i % 5 != 4) - { - ImGui.SameLine(); - } - } - - ImGui.EndTabItem(); - } - } - - private static void RenderUtilityTab() - { - if (ImGui.BeginTabItem("Utilities & Tools")) - { - // File operations - ImGui.Text("File Operations:"); - ImGui.InputText("File Path", ref filePath, 256); - ImGui.SameLine(); - if (ImGui.Button("Load") && !string.IsNullOrEmpty(filePath)) - { - try - { - fileContent = File.ReadAllText(filePath); - } - catch (Exception ex) when (ex is FileNotFoundException or UnauthorizedAccessException) - { - // Handle file read errors gracefully - fileContent = $"Error loading file: {ex.Message}"; - } - } - - if (!string.IsNullOrEmpty(fileContent)) - { - ImGui.Text("File Content Preview:"); - ImGui.TextWrapped(fileContent.Length > 500 ? fileContent[..500] + "..." : fileContent); - } - - ImGui.Separator(); - - // System information - ImGui.Text("System Information:"); - unsafe - { - byte* ptr = ImGui.GetVersion(); - int length = 0; - while (ptr[length] != 0) - { - length++; - } - ImGui.Text($"ImGui Version: {Encoding.UTF8.GetString(ptr, length)}"); - } - ImGui.Text($"Display Size: {ImGui.GetIO().DisplaySize}"); - - ImGui.Separator(); - - // Debugging tools - ImGui.Text("Debug Tools:"); - if (ImGui.Button("Show ImGui Demo")) - { - showImGuiDemo = true; - } - ImGui.SameLine(); - if (ImGui.Button("Show Style Editor")) - { - showStyleEditor = true; - } - ImGui.SameLine(); - if (ImGui.Button("Show Metrics")) - { - showMetrics = true; - } - - ImGui.EndTabItem(); - } - } - - private static void RenderUnicodeTab() - { - if (ImGui.BeginTabItem("Unicode & Emojis")) - { - ImGui.TextWrapped("Unicode and Emoji Support (Enabled by Default)"); - ImGui.TextWrapped("ImGuiApp automatically includes support for Unicode characters and emojis. This feature works with your configured fonts."); - ImGui.Separator(); - - ImGui.Text("Basic ASCII: Hello World!"); - ImGui.Text("Accented characters: cafÊ, naïve, rÊsumÊ"); - ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ â‰Ĩ Âą × Ãˇ ∂ ∑ ∏ √ âˆĢ"); - ImGui.Text("Currency symbols: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ"); - ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ⇐ ⇒ ⇑ ⇓"); - ImGui.Text("Geometric shapes: ■ □ ▲ â–ŗ ● ○ ◆ ◇ ★ ☆"); - ImGui.Text("Miscellaneous symbols: ♠ â™Ŗ â™Ĩ â™Ļ ☀ ☁ ☂ ☃ â™Ē â™Ģ"); - - ImGui.Separator(); - ImGui.Text("Full Emoji Range Support (if font supports them):"); - ImGui.Text("Faces: 😀 😃 😄 😁 😆 😅 😂 đŸ¤Ŗ 😊 😇 😍 😎 🤓 🧐 🤔 😴"); - ImGui.Text("Gestures: 👍 👎 👌 âœŒī¸ 🤞 🤟 🤘 🤙 👈 👉 👆 👇 â˜ī¸ ✋ 🤚 🖐"); - ImGui.Text("Objects: 🚀 đŸ’ģ 📱 🎸 🎨 🏆 🌟 💎 ⚡ đŸ”Ĩ 💡 🔧 âš™ī¸ 🔑 💰"); - ImGui.Text("Nature: 🌈 🌞 🌙 ⭐ 🌍 🌊 đŸŒŗ 🌸 đŸĻ‹ 🐝 đŸļ 🐱 đŸĻŠ đŸģ đŸŧ"); - ImGui.Text("Food: 🍎 🍌 🍕 🍔 🍟 đŸĻ 🎂 ☕ đŸē 🍷 🍓 đŸĨ‘ đŸĨ¨ 🧀 đŸ¯"); - ImGui.Text("Transport: 🚗 🚂 âœˆī¸ 🚲 đŸšĸ 🚁 🚌 đŸī¸ 🛸 🚜 đŸŽī¸ 🚙 🚕 🚐"); - ImGui.Text("Activities: âšŊ 🏀 🏈 ⚾ 🎾 🏐 🏉 🎱 🏓 🏸 đŸĨŠ â›ŗ đŸŽ¯ đŸŽĒ"); - ImGui.Text("Weather: â˜€ī¸ ⛅ â˜ī¸ đŸŒ¤ī¸ â›ˆī¸ đŸŒ§ī¸ â„ī¸ â˜ƒī¸ ⛄ đŸŒŦī¸ 💨 🌊 💧"); - ImGui.Text("Symbols: â¤ī¸ 💚 💙 💜 🖤 💛 💔 âŖī¸ 💕 💖 💗 💘 💝 ✨"); - ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙ â¤´ī¸ â¤ĩī¸ 🔀 🔁 🔂 🔄 🔃"); - ImGui.Text("Math: Âą × Ãˇ = ≠ ≈ ≤ â‰Ĩ ∞ √ ∑ ∏ ∂ âˆĢ Ί Ī€ Îą β Îŗ δ"); - ImGui.Text("Geometric: ■ □ ▲ â–ŗ ● ○ ◆ ◇ ★ ☆ ♠ â™Ŗ â™Ĩ â™Ļ â–Ē â–Ģ ◾ â—Ŋ"); - ImGui.Text("Currency: $ â‚Ŧ ÂŖ ÂĨ ₹ â‚ŋ Âĸ â‚Ŋ ₩ ₡ â‚Ē â‚Ģ ₱ ₴ â‚Ļ ₨ â‚ĩ"); - ImGui.Text("Dingbats: ✂ ✈ ☎ ⌚ ⏰ âŗ ⌛ ⚡ ☔ ☂ ☀ ⭐ ☁ ⛅ ❄"); - ImGui.Text("Enclosed: ① ② â‘ĸ â‘Ŗ ⑤ â‘Ĩ â‘Ļ ⑧ ⑨ ⑩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ"); - - ImGui.Separator(); - ImGui.TextWrapped("Note: Character display depends on your configured font's Unicode support. " + - "If characters show as question marks, your font may not include those glyphs."); - - ImGui.Separator(); - ImGui.TextWrapped("To disable Unicode support (ASCII only), set EnableUnicodeSupport = false in your ImGuiAppConfig."); - - ImGui.EndTabItem(); - } - } - - private static void RenderNerdFontTab() - { - if (ImGui.BeginTabItem("Nerd Fonts")) - { - ImGui.TextWrapped("Nerd Font Icons (Patched Fonts)"); - ImGui.TextWrapped("This tab demonstrates Nerd Font icons if you're using a Nerd Font (like JetBrains Mono Nerd Font, Fira Code Nerd Font, etc.)."); - ImGui.Separator(); - - // Powerline symbols - ImGui.Text("Powerline Symbols:"); - ImGui.Text("Basic: \uE0A0 \uE0A1 \uE0A2 \uE0B0 \uE0B1 \uE0B2 \uE0B3"); - ImGui.Text("Extra: \uE0A3 \uE0B4 \uE0B5 \uE0B6 \uE0B7 \uE0B8 \uE0CA \uE0CC \uE0CD \uE0D0 \uE0D1 \uE0D4"); - - ImGui.Separator(); - - // Font Awesome icons - ImGui.Text("Font Awesome Icons:"); - ImGui.Text("Files & Folders: \uF07B \uF07C \uF15B \uF15C \uF016 \uF017 \uF019 \uF01A \uF093 \uF095"); - ImGui.Text("Git & Version Control: \uF1D3 \uF1D2 \uF126 \uF127 \uF128 \uF129 \uF12A \uF12B"); - ImGui.Text("Media & UI: \uF04B \uF04C \uF04D \uF050 \uF051 \uF048 \uF049 \uF067 \uF068 \uF00C \uF00D"); - - ImGui.Separator(); - - // Material Design icons - ImGui.Text("Material Design Icons:"); - ImGui.Text("Navigation: \uF52A \uF52B \uF544 \uF53F \uF540 \uF541 \uF542 \uF543"); - ImGui.Text("Actions: \uF8D5 \uF8D6 \uF8D7 \uF8D8 \uF8D9 \uF8DA \uF8DB \uF8DC"); - ImGui.Text("Content: \uF1C1 \uF1C2 \uF1C3 \uF1C4 \uF1C5 \uF1C6 \uF1C7 \uF1C8"); - - ImGui.Separator(); - - // Weather icons - ImGui.Text("Weather Icons:"); - ImGui.Text("Basic Weather: \uE30D \uE30E \uE30F \uE310 \uE311 \uE312 \uE313 \uE314"); - ImGui.Text("Temperature: \uE315 \uE316 \uE317 \uE318 \uE319 \uE31A \uE31B \uE31C"); - ImGui.Text("Wind & Pressure: \uE31D \uE31E \uE31F \uE320 \uE321 \uE322 \uE323 \uE324"); - - ImGui.Separator(); - - // Devicons - ImGui.Text("Developer Icons (Devicons):"); - ImGui.Text("Languages: \uE73C \uE73D \uE73E \uE73F \uE740 \uE741 \uE742 \uE743"); // Various programming languages - ImGui.Text("Frameworks: \uE744 \uE745 \uE746 \uE747 \uE748 \uE749 \uE74A \uE74B"); - ImGui.Text("Tools: \uE74C \uE74D \uE74E \uE74F \uE750 \uE751 \uE752 \uE753"); - - ImGui.Separator(); - - // Octicons - ImGui.Text("Octicons (GitHub Icons):"); - ImGui.Text("Version Control: \uF418 \uF419 \uF41A \uF41B \uF41C \uF41D \uF41E \uF41F"); - ImGui.Text("Issues & PRs: \uF420 \uF421 \uF422 \uF423 \uF424 \uF425 \uF426 \uF427"); - ImGui.Text("Social: \u2665 \u26A1 \uF428 \uF429 \uF42A \uF42B \uF42C \uF42D"); - - ImGui.Separator(); - - // Font Logos - ImGui.Text("Brand Logos (Font Logos):"); - ImGui.Text("Tech Brands: \uF300 \uF301 \uF302 \uF303 \uF304 \uF305 \uF306 \uF307"); - ImGui.Text("More Logos: \uF308 \uF309 \uF30A \uF30B \uF30C \uF30D \uF30E \uF30F"); - - ImGui.Separator(); - - // Pomicons - ImGui.Text("Pomicons:"); - ImGui.Text("Small Icons: \uE000 \uE001 \uE002 \uE003 \uE004 \uE005 \uE006 \uE007"); - ImGui.Text("More Icons: \uE008 \uE009 \uE00A \uE00B \uE00C \uE00D"); - - ImGui.Separator(); - ImGui.TextWrapped("Note: These icons will only display correctly if you're using a Nerd Font. " + - "If you see question marks or boxes, switch to a Nerd Font like 'JetBrains Mono Nerd Font' or 'Fira Code Nerd Font'."); - - ImGui.Separator(); - ImGui.TextWrapped("Popular Nerd Fonts: JetBrains Mono Nerd Font, Fira Code Nerd Font, Hack Nerd Font, " + - "Source Code Pro Nerd Font, DejaVu Sans Mono Nerd Font, and many more at nerdfonts.com"); - - ImGui.EndTabItem(); - } - } - - private static void RenderModalAndPopups() - { - // Modal dialog - if (showModal) - { - ImGui.OpenPopup("Demo Modal"); - showModal = false; - } - - if (ImGui.BeginPopupModal("Demo Modal", ref showModal)) - { - ImGui.Text("This is a modal dialog."); - ImGui.Text("It blocks interaction with the main window."); - ImGui.Separator(); - - ImGui.InputText("Input", ref modalInputBuffer, 100); - - if (ImGui.Button("OK")) - { - modalResult = $"You entered: {modalInputBuffer}"; - ImGui.CloseCurrentPopup(); - } - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - { - modalResult = "Cancelled"; - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - // Context popup - if (showPopup) - { - ImGui.OpenPopup("Demo Popup"); - showPopup = false; - } - - if (ImGui.BeginPopup("Demo Popup")) - { - ImGui.Text("This is a popup menu"); - if (ImGui.MenuItem("Option 1")) - { - // Handle option 1 - } - if (ImGui.MenuItem("Option 2")) - { - // Handle option 2 - } - ImGui.Separator(); - if (ImGui.MenuItem("Close")) - { - // Handle close - } - ImGui.EndPopup(); - } - } - - private static void UpdateAnimations(float dt) - { - animationTime += dt; - - // Bouncing animation - bounceOffset = (MathF.Abs(MathF.Sin(animationTime * 3)) * 50) - 25; - - // Pulse animation - pulseScale = 0.8f + (0.4f * MathF.Sin(animationTime * 4)); - } - - private static void RenderAboutWindow() - { - ImGui.Begin("About ImGuiApp Demo", ref showAbout); - ImGui.Text("ImGuiApp Demo Application"); - ImGui.Separator(); - ImGui.Text("This demo showcases extensive ImGui.NET features including:"); - ImGui.BulletText("Basic and advanced widgets"); - ImGui.BulletText("Layout systems (columns, tables, tabs)"); - ImGui.BulletText("Custom graphics and drawing"); - ImGui.BulletText("Data visualization and plotting"); - ImGui.BulletText("Input handling and interaction"); - ImGui.BulletText("Animations and effects"); - ImGui.BulletText("File operations and utilities"); - ImGui.Separator(); - ImGui.Text("Built with:"); - ImGui.BulletText("Hexa.NET.ImGui"); - ImGui.BulletText("Silk.NET"); - ImGui.BulletText("ktsu.ImGui Framework"); - ImGui.End(); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "This is a demo application")] - private static void UpdatePlotData(float dt) - { - plotRefreshTime += dt; - if (plotRefreshTime >= 0.1f) // Update every 100ms - { - plotRefreshTime = 0; - plotValues.Add((float)random.NextDouble()); - if (plotValues.Count > 100) // Keep last 100 values - { - plotValues.RemoveAt(0); - } - } - } - - private static void OnAppMenu() - { - if (ImGui.BeginMenu("View")) - { - ImGui.MenuItem("ImGui Demo", string.Empty, ref showImGuiDemo); - ImGui.MenuItem("Style Editor", string.Empty, ref showStyleEditor); - ImGui.MenuItem("Metrics", string.Empty, ref showMetrics); - ImGui.EndMenu(); - } - - if (ImGui.BeginMenu("Help")) - { - ImGui.MenuItem("About", string.Empty, ref showAbout); - ImGui.EndMenu(); - } - } -} diff --git a/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.cs b/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.cs deleted file mode 100644 index 7c044d0..0000000 --- a/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.cs +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Examples.Popups; - -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.App; -using ktsu.ImGui.Popups; - -internal static class ImGuiPopupsDemo -{ - private static void Main() - { - ImGuiApp.Start(new() - { - Title = "ImGui Popups Demo", - OnAppMenu = OnAppMenu, - OnMoveOrResize = OnMoveOrResize, - OnStart = OnStart, - OnRender = OnRender, - SaveIniSettings = false, - }); - } - - // Demo state variables - private static string stringInputValue = "Hello World"; - private static int intInputValue = 42; - private static float floatInputValue = 3.14159f; - private static string selectedFriend = "None"; - private static string selectedColor = "None"; - private static string lastFileOpened = "None"; - private static string lastFileSaved = "None"; - private static string lastDirectoryChosen = "None"; - private static string lastPromptResult = "None"; - private static string lastCustomModalResult = "None"; - - // Sample data - private static readonly string[] Friends = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"]; - private static readonly string[] Colors = ["Red", "Green", "Blue", "Yellow", "Purple", "Orange", "Pink", "Cyan", "Magenta", "Brown"]; - - // Custom modal state variables - private static bool customCheckbox; - private static float customSlider = 0.5f; - private static readonly string[] advancedModalItems = ["Option 1", "Option 2", "Option 3", "Option 4"]; - private static int advancedModalSelectedItem; - private static Vector3 advancedModalColorValue = new(1.0f, 0.5f, 0.0f); - private static readonly bool[] advancedModalFlags = [true, false, true, false]; - - // Popup instances - private static readonly ImGuiPopups.InputString popupInputString = new(); - private static readonly ImGuiPopups.InputInt popupInputInt = new(); - private static readonly ImGuiPopups.InputFloat popupInputFloat = new(); - private static readonly ImGuiPopups.FilesystemBrowser popupFilesystemBrowser = new(); - private static readonly ImGuiPopups.MessageOK popupMessageOK = new(); - private static readonly ImGuiPopups.SearchableList popupSearchableListFriends = new(); - private static readonly ImGuiPopups.SearchableList popupSearchableListColors = new(); - private static readonly ImGuiPopups.Prompt popupPrompt = new(); - private static readonly ImGuiPopups.Modal popupCustomModal = new(); - - private static void OnStart() - { - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] - private static void OnRender(float dt) - { - ImGui.Text("ImGui Popups Library - Demo"); - ImGui.Text("This demo showcases all popup types and configurations available in the library."); - ImGui.Separator(); - - RenderInputPopupsSection(); - RenderMessageAndPromptSection(); - RenderSearchableListsSection(); - RenderFileSystemBrowserSection(); - RenderCustomModalSection(); - RenderAdvancedExamplesSection(); - RenderTipsSection(); - - // Show all open popups - ShowAllPopups(); - } - - private static void RenderInputPopupsSection() - { - if (ImGui.CollapsingHeader("Input Popups", ImGuiTreeNodeFlags.DefaultOpen)) - { - ImGui.Text("Input popups allow users to enter different types of values."); - ImGui.Spacing(); - - // String Input - ImGui.Text($"Current String Value: {stringInputValue}"); - if (ImGui.Button("Edit String")) - { - popupInputString.Open("Edit String Value", "Enter a new string:", stringInputValue, result => stringInputValue = result); - } - - ImGui.SameLine(); - if (ImGui.Button("Edit String (Custom Size)")) - { - popupInputString.Open("Edit String Value", "Enter a new string:", stringInputValue, result => stringInputValue = result, new Vector2(400, 150)); - } - - // Integer Input - ImGui.Text($"Current Integer Value: {intInputValue}"); - if (ImGui.Button("Edit Integer")) - { - popupInputInt.Open("Edit Integer Value", "Enter a new integer:", intInputValue, result => intInputValue = result); - } - - // Float Input - ImGui.Text($"Current Float Value: {floatInputValue:F5}"); - if (ImGui.Button("Edit Float")) - { - popupInputFloat.Open("Edit Float Value", "Enter a new float:", floatInputValue, result => floatInputValue = result); - } - - ImGui.Spacing(); - } - } - - private static void RenderMessageAndPromptSection() - { - if (ImGui.CollapsingHeader("Message & Prompt Popups", ImGuiTreeNodeFlags.DefaultOpen)) - { - ImGui.Text("Display messages and custom prompts with various configurations."); - ImGui.Spacing(); - - // Simple Message - if (ImGui.Button("Show Simple Message")) - { - popupMessageOK.Open("Information", "This is a simple informational message popup."); - } - - ImGui.SameLine(); - if (ImGui.Button("Show Long Message")) - { - string longMessage = @"This is a very long message that demonstrates text wrapping capabilities. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."; - - popupMessageOK.Open("Long Message", longMessage, ImGuiPopups.PromptTextLayoutType.Wrapped, new Vector2(500, 300)); - } - - // Custom Prompt with Multiple Buttons - ImGui.Text($"Last Prompt Result: {lastPromptResult}"); - if (ImGui.Button("Show Custom Prompt")) - { - Dictionary buttons = new() - { - { "Yes", () => lastPromptResult = "User clicked Yes" }, - { "No", () => lastPromptResult = "User clicked No" }, - { "Maybe", () => lastPromptResult = "User clicked Maybe" }, - { "Cancel", () => lastPromptResult = "User clicked Cancel" } - }; - popupPrompt.Open("Confirmation", "Do you want to proceed with this action?", buttons); - } - - ImGui.SameLine(); - if (ImGui.Button("Show Warning Prompt")) - { - Dictionary buttons = new() - { - { "Delete", () => lastPromptResult = "User confirmed deletion" }, - { "Cancel", () => lastPromptResult = "User cancelled deletion" } - }; - string warning = "âš ī¸ WARNING: This action cannot be undone!\n\nAre you sure you want to delete all selected files?"; - popupPrompt.Open("Confirm Deletion", warning, buttons, ImGuiPopups.PromptTextLayoutType.Wrapped, new Vector2(400, 200)); - } - - ImGui.Spacing(); - } - } - - private static void RenderSearchableListsSection() - { - if (ImGui.CollapsingHeader("Searchable Lists", ImGuiTreeNodeFlags.DefaultOpen)) - { - ImGui.Text("Select items from searchable and filterable lists."); - ImGui.Spacing(); - - ImGui.Text($"Selected Friend: {selectedFriend}"); - if (ImGui.Button("Choose Friend")) - { - popupSearchableListFriends.Open("Choose Friend", "Select your best friend:", Friends, result => selectedFriend = result); - } - - ImGui.SameLine(); - if (ImGui.Button("Choose Friend (Custom Size)")) - { - popupSearchableListFriends.Open("Choose Friend", "Select your best friend:", Friends, null, null, result => selectedFriend = result, new Vector2(350, 400)); - } - - ImGui.Text($"Selected Color: {selectedColor}"); - if (ImGui.Button("Choose Color")) - { - popupSearchableListColors.Open("Choose Color", "Select your favorite color:", Colors, - item => $"🎨 {item}", result => selectedColor = result); - } - - ImGui.Spacing(); - } - } - - private static void RenderFileSystemBrowserSection() - { - if (ImGui.CollapsingHeader("File System Browser", ImGuiTreeNodeFlags.DefaultOpen)) - { - ImGui.Text("Browse and select files or directories with filtering support."); - ImGui.Spacing(); - - // File Operations - ImGui.Text($"Last File Opened: {lastFileOpened}"); - if (ImGui.Button("Open Any File")) - { - popupFilesystemBrowser.FileOpen("Open File", file => lastFileOpened = file.ToString()); - } - - ImGui.SameLine(); - if (ImGui.Button("Open C# File")) - { - popupFilesystemBrowser.FileOpen("Open C# File", file => lastFileOpened = file.ToString(), "*.cs"); - } - - ImGui.SameLine(); - if (ImGui.Button("Open Image File")) - { - popupFilesystemBrowser.FileOpen("Open Image File", file => lastFileOpened = file.ToString(), new Vector2(600, 500), "*.{png,jpg,jpeg,gif,bmp}"); - } - - ImGui.Text($"Last File Saved: {lastFileSaved}"); - if (ImGui.Button("Save Text File")) - { - popupFilesystemBrowser.FileSave("Save Text File", file => lastFileSaved = file.ToString(), "*.txt"); - } - - ImGui.SameLine(); - if (ImGui.Button("Save Any File")) - { - popupFilesystemBrowser.FileSave("Save File", file => lastFileSaved = file.ToString()); - } - - // Directory Operations - ImGui.Text($"Last Directory Chosen: {lastDirectoryChosen}"); - if (ImGui.Button("Choose Directory")) - { - popupFilesystemBrowser.ChooseDirectory("Choose Directory", directory => lastDirectoryChosen = directory.ToString()); - } - - ImGui.SameLine(); - if (ImGui.Button("Choose Directory (Large)")) - { - popupFilesystemBrowser.ChooseDirectory("Choose Directory", directory => lastDirectoryChosen = directory.ToString(), new Vector2(700, 600)); - } - - ImGui.Spacing(); - } - } - - private static void RenderCustomModalSection() - { - if (ImGui.CollapsingHeader("Custom Modal", ImGuiTreeNodeFlags.DefaultOpen)) - { - ImGui.Text("Create completely custom modal content with full control."); - ImGui.Spacing(); - - ImGui.Text($"Last Custom Modal Result: {lastCustomModalResult}"); - if (ImGui.Button("Show Custom Modal")) - { - popupCustomModal.Open("Custom Modal Example", ShowCustomModalContent); - } - - ImGui.SameLine(); - if (ImGui.Button("Show Custom Modal (Large)")) - { - popupCustomModal.Open("Advanced Custom Modal", ShowAdvancedCustomModalContent, new Vector2(600, 400)); - } - - ImGui.Spacing(); - } - } - - private static void RenderAdvancedExamplesSection() - { - if (ImGui.CollapsingHeader("Advanced Examples")) - { - ImGui.Text("Complex usage patterns and edge cases."); - ImGui.Spacing(); - - if (ImGui.Button("Nested Popup Example")) - { - popupMessageOK.Open("First Popup", "This popup will open another popup when you click OK."); - } - - ImGui.SameLine(); - if (ImGui.Button("Validation Example")) - { - popupInputString.Open("Enter Email", "Please enter a valid email address:", "", result => - { - if (result.Contains('@') && result.Contains('.')) - { - stringInputValue = result; - popupMessageOK.Open("Success", $"Email '{result}' is valid!"); - } - else - { - popupMessageOK.Open("Error", "Invalid email format! Please try again."); - } - }); - } - - if (ImGui.Button("Multi-Step Workflow")) - { - StartMultiStepWorkflow(); - } - - ImGui.Spacing(); - } - } - - private static void RenderTipsSection() - { - if (ImGui.CollapsingHeader("Tips & Features")) - { - ImGui.TextWrapped("â€ĸ Press ESC to close any popup"); - ImGui.TextWrapped("â€ĸ Use TAB to navigate between input fields"); - ImGui.TextWrapped("â€ĸ Enter key confirms string inputs"); - ImGui.TextWrapped("â€ĸ Double-click items in file browser to open/navigate"); - ImGui.TextWrapped("â€ĸ Type to search in searchable lists"); - ImGui.TextWrapped("â€ĸ All popups support custom sizing"); - ImGui.TextWrapped("â€ĸ Text can be wrapped or unformatted"); - ImGui.TextWrapped("â€ĸ File browser supports glob patterns for filtering"); - } - } - - private static void ShowCustomModalContent() - { - ImGui.Text("This is a custom modal with your own content!"); - ImGui.Separator(); - - ImGui.Checkbox("Custom Checkbox", ref customCheckbox); - ImGui.SliderFloat("Custom Slider", ref customSlider, 0.0f, 1.0f); - - ImGui.NewLine(); - - if (ImGui.Button("Set Result")) - { - lastCustomModalResult = $"Checkbox: {customCheckbox}, Slider: {customSlider:F2}"; - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (ImGui.Button("Close")) - { - lastCustomModalResult = "User closed without setting result"; - ImGui.CloseCurrentPopup(); - } - } - - private static void ShowAdvancedCustomModalContent() - { - ImGui.Text("Advanced Custom Modal with Multiple Controls"); - ImGui.Separator(); - - ImGui.Combo("Select Option", ref advancedModalSelectedItem, advancedModalItems, advancedModalItems.Length); - ImGui.ColorEdit3("Color Picker", ref advancedModalColorValue); - - ImGui.Text("Flags:"); - for (int i = 0; i < advancedModalFlags.Length; i++) - { - ImGui.Checkbox($"Flag {i + 1}", ref advancedModalFlags[i]); - if (i < advancedModalFlags.Length - 1) - { - ImGui.SameLine(); - } - } - - ImGui.Separator(); - - if (ImGui.Button("Apply Settings")) - { - lastCustomModalResult = $"Selected: {advancedModalItems[advancedModalSelectedItem]}, Color: RGB({advancedModalColorValue.X:F2}, {advancedModalColorValue.Y:F2}, {advancedModalColorValue.Z:F2})"; - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (ImGui.Button("Reset")) - { - advancedModalSelectedItem = 0; - advancedModalColorValue = new Vector3(1.0f, 0.5f, 0.0f); - for (int i = 0; i < advancedModalFlags.Length; i++) - { - advancedModalFlags[i] = i % 2 == 0; - } - } - - ImGui.SameLine(); - if (ImGui.Button("Cancel")) - { - lastCustomModalResult = "User cancelled"; - ImGui.CloseCurrentPopup(); - } - } - - private static void StartMultiStepWorkflow() - { - popupInputString.Open("Step 1: Enter Name", "What's your name?", "", name => - { - if (string.IsNullOrWhiteSpace(name)) - { - popupMessageOK.Open("Error", "Name cannot be empty!"); - return; - } - - popupSearchableListFriends.Open("Step 2: Choose Friend", $"Hi {name}! Who would you like to invite?", Friends, friend => - { - Dictionary buttons = new() - { - { "Send Invitation", () => - { - lastPromptResult = $"Invitation sent to {friend} from {name}"; - popupMessageOK.Open("Success", $"Invitation sent to {friend}!"); - } - }, - { "Cancel", () => lastPromptResult = "Workflow cancelled" } - }; - - popupPrompt.Open("Step 3: Confirm", $"{name}, send invitation to {friend}?", buttons); - }); - }); - } - - private static void ShowAllPopups() - { - // Show all popup instances - popupInputString.ShowIfOpen(); - popupInputInt.ShowIfOpen(); - popupInputFloat.ShowIfOpen(); - popupMessageOK.ShowIfOpen(); - popupSearchableListFriends.ShowIfOpen(); - popupSearchableListColors.ShowIfOpen(); - popupPrompt.ShowIfOpen(); - popupFilesystemBrowser.ShowIfOpen(); - popupCustomModal.ShowIfOpen(); - } - - private static void OnAppMenu() - { - if (ImGui.BeginMenu("Demo")) - { - if (ImGui.MenuItem("Reset All Values")) - { - stringInputValue = "Hello World"; - intInputValue = 42; - floatInputValue = 3.14159f; - selectedFriend = "None"; - selectedColor = "None"; - lastFileOpened = "None"; - lastFileSaved = "None"; - lastDirectoryChosen = "None"; - lastPromptResult = "None"; - lastCustomModalResult = "None"; - } - - if (ImGui.MenuItem("About")) - { - popupMessageOK.Open("About ImGui Popups Demo", - "ImGui Popups Library Demo\n\nThis comprehensive demo showcases all features of the ktsu.ImGuiPopups library, including:\n\n" + - "â€ĸ Input popups for strings, integers, and floats\n" + - "â€ĸ Message and confirmation prompts\n" + - "â€ĸ Searchable list selection\n" + - "â€ĸ File and directory browsers\n" + - "â€ĸ Custom modal content\n" + - "â€ĸ Advanced usage patterns\n\n" + - "Press ESC to close any popup, or use the provided buttons.", - ImGuiPopups.PromptTextLayoutType.Wrapped, - new Vector2(450, 350)); - } - - ImGui.EndMenu(); - } - } - - private static void OnMoveOrResize() - { - // Method intentionally left empty. - } -} diff --git a/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.csproj b/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.csproj deleted file mode 100644 index cb6779e..0000000 --- a/examples/ImGuiPopupsDemo/ImGuiPopupsDemo.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - net9.0 - - - - - - - - diff --git a/examples/ImGuiStylerDemo/ImGuiStylerDemo.cs b/examples/ImGuiStylerDemo/ImGuiStylerDemo.cs deleted file mode 100644 index 0a7535a..0000000 --- a/examples/ImGuiStylerDemo/ImGuiStylerDemo.cs +++ /dev/null @@ -1,972 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Examples.Styler; - -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -using Hexa.NET.ImGui; - -using ktsu.ImGui.App; -using ktsu.ImGui.Styler; -using ktsu.ThemeProvider; - -/// -/// Comprehensive demonstration of the ImGuiStyler library capabilities. -/// -internal sealed class ImGuiStylerDemo -{ - private static bool valueBool = true; - private static bool valueBool2; - private static int valueInt = 42; - private static string valueString = "Hello ImGuiStyler!"; - private static float valueFloat = 0.5f; - private static float valueFloat2 = 0.75f; - private static int selectedFamily; - private static readonly float[] colorValue = [0.4f, 0.7f, 0.0f, 1.0f]; - private static readonly string[] comboItems = ["Option A", "Option B", "Option C", "Option D"]; - private static int comboSelection; - private static int radioSelection = 1; - private static readonly float[] plotData = [0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f]; - - // Form validation fields - private static string formUsername = ""; - private static string formEmail = ""; - - // Use ThemeProvider registry for all available themes - private static readonly List availableThemes = [.. Theme.AllThemes]; - private static readonly List availableFamilies = [.. Theme.Families.OrderBy(f => f)]; - - // Store the currently selected theme instead of applying it globally - private static ThemeRegistry.ThemeInfo? currentSelectedTheme; - - private static void Main() - { - ImGuiStylerDemo demo = new(); - ImGuiApp.Start(new() - { - Title = "ImGuiStyler Demo - Comprehensive Theme & Color Showcase", - OnAppMenu = demo.OnAppMenu, - OnMoveOrResize = demo.OnMoveOrResize, - OnRender = demo.OnRender, - OnStart = demo.OnStart, - FrameWrapperFactory = () => currentSelectedTheme is null ? null : new ScopedTheme(currentSelectedTheme.CreateInstance()), - SaveIniSettings = false, - }); - } - - private void OnStart() - { - // Keep default ImGui styling - we'll use scoped themes for demonstration - // currentSelectedTheme starts as null, so we'll show default styling - } - - private void OnRender(float dt) - { - // Header with current theme info - if (currentSelectedTheme is not null) - { - ImGui.Text($"🎨 Current Theme: {currentSelectedTheme.Name}"); - ImGui.SameLine(); - ImGui.Text($"({(currentSelectedTheme.IsDark ? "Dark" : "Light")})"); - } - else - { - ImGui.Text("🎨 Current Theme: Default (Reset)"); - } - - ImGui.Separator(); - - // Render the library's theme selection dialog if it's open - // This now returns true if a theme was changed during modal interaction - if (Theme.RenderThemeSelector()) - { - // Theme was changed via the modal browser - respond to the change - if (Theme.CurrentThemeName is null) - { - Console.WriteLine("Theme reset to default via modal"); - currentSelectedTheme = null; // Update our local selection - } - else - { - Console.WriteLine($"Theme changed via modal to: {Theme.CurrentThemeName}"); - // Find and store the corresponding theme info - currentSelectedTheme = availableThemes.FirstOrDefault(t => t.Name == Theme.CurrentThemeName); - } - } - - if (ImGui.BeginTabBar("DemoTabs")) - { - if (ImGui.BeginTabItem("🎨 Theme Gallery")) - { - ShowThemeGallery(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("🎨 Color Palettes")) - { - ShowColorPalettesDemo(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("🔍 Complete Theme Palette")) - { - ShowCompleteThemePalette(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("đŸ–ąī¸ Widget Showcase")) - { - ShowWidgetShowcase(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("💡 Interactive Examples")) - { - ShowInteractiveExamples(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("📚 Documentation")) - { - ShowDocumentationDemo(); - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - } - - private static void ShowThemeGallery() - { - ImGui.TextUnformatted("🎨 Theme Gallery"); - ImGui.Text("Explore all available themes from the ThemeProvider registry."); - ImGui.Separator(); - - // Theme family selector - ImGui.Text("Filter by Family:"); - if (ImGui.Combo("##Family", ref selectedFamily, ["All", .. availableFamilies], availableFamilies.Count + 1)) - { - // selectedTheme = 0; // Reset selection when family changes - } - - ImGui.Separator(); - - // Get filtered themes - IEnumerable filteredThemes = selectedFamily == 0 - ? availableThemes - : availableThemes.Where(t => t.Family == availableFamilies[selectedFamily - 1]); - - List themesToShow = [.. filteredThemes]; - - // Theme grid - ImGui.Text($"Available Themes ({themesToShow.Count}):"); - - // Add reset button - if (ImGui.Button("Reset to Default")) - { - currentSelectedTheme = null; - } - ImGui.SameLine(); - ImGui.Text("(or click a theme below to apply it)"); - - ImGui.BeginChild("ThemeGrid", new Vector2(0, 300), ImGuiChildFlags.Borders); - - // Use the new delegate-based ThemeCard widget from the library - ThemeCard.RenderGrid(themesToShow, selectedTheme => currentSelectedTheme = selectedTheme); - - ImGui.EndChild(); - - ImGui.Separator(); - - // Quick theme preview with widgets - ImGui.Text("Theme Preview:"); - ImGui.BeginChild("PreviewArea", new Vector2(0, 200), ImGuiChildFlags.Borders); - - ImGui.Text("Sample UI Elements:"); - if (ImGui.Button("Sample Button")) - { - // Button clicked - } - ImGui.SameLine(); - if (ImGui.SmallButton("Small")) - { - // Small button clicked - } - - ImGui.Checkbox("Sample Checkbox", ref valueBool); - ImGui.SliderFloat("Sample Slider", ref valueFloat, 0.0f, 1.0f); - ImGui.InputText("Sample Input", ref valueString, 128); - - ImGui.Text("Radio Buttons:"); - ImGui.RadioButton("Option 1", ref radioSelection, 0); - ImGui.SameLine(); - ImGui.RadioButton("Option 2", ref radioSelection, 1); - ImGui.SameLine(); - ImGui.RadioButton("Option 3", ref radioSelection, 2); - - ImGui.EndChild(); - } - - private static void ShowCompleteThemePalette() - { - ImGui.TextUnformatted("🔍 Complete Theme Palette"); - ImGui.Text("Explore every color available in the current theme using the new MakeCompletePalette API."); - ImGui.Separator(); - - // Check if a theme is active - IReadOnlyDictionary? completePalette = Theme.GetCurrentThemeCompletePalette(); - if (completePalette is null) - { - ImGui.TextWrapped("No theme is currently active. Select a theme from the Theme Gallery tab to see its complete palette."); - return; - } - - ImGui.Text($"Theme: {Theme.CurrentThemeName} - {completePalette.Count} colors available"); - ImGui.Separator(); - - // Help text moved above the table - ImGui.TextUnformatted("💡 Tip: Click on any color swatch to copy its hex value to clipboard"); - ImGui.TextUnformatted("💡 Hover over swatches to see detailed color information and usage examples"); - ImGui.TextUnformatted("⚙ Icon overlay shows contrast test using highest priority neutral color on all backgrounds"); - ImGui.TextUnformatted("📊 Table shows semantic meanings (rows) × priorities (columns, VeryLow→VeryHigh) for easy comparison"); - ImGui.Separator(); - - // Get all unique semantic meanings and priorities - HashSet allMeanings = [.. completePalette.Keys.Select(k => k.Meaning)]; - HashSet allPriorities = [.. completePalette.Keys.Select(k => k.Priority)]; - - // Sort meanings and priorities - List sortedMeanings = [.. allMeanings.OrderBy(m => m.ToString())]; - List sortedPriorities = [.. allPriorities.OrderBy(p => p)]; // Lowest first (inverted) - - // Find the highest priority neutral color for icon overlay - Priority? highestNeutralPriority = completePalette.Keys - .Where(k => k.Meaning == SemanticMeaning.Neutral) - .Select(k => k.Priority) - .DefaultIfEmpty() - .Max(); - - ImColor? neutralIconColor = null; - if (highestNeutralPriority.HasValue && - completePalette.TryGetValue(new(SemanticMeaning.Neutral, highestNeutralPriority.Value), out PerceptualColor neutralColor)) - { - neutralIconColor = Color.FromPerceptualColor(neutralColor); - } - - // Create table with semantic meanings as rows and priorities as columns - const float swatchWidth = 90.0f; - const float swatchHeight = 45.0f; - - // Calculate exact column count to avoid mismatches - int totalColumns = sortedPriorities.Count + 1; // +1 for semantic name column - - ImGui.BeginGroup(); - if (ImGui.BeginTable("ColorPaletteTable", totalColumns, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) - { - // Set up columns - ImGui.TableSetupColumn("Semantic", ImGuiTableColumnFlags.WidthFixed, 120.0f); - foreach (Priority priority in sortedPriorities) - { - ImGui.TableSetupColumn(priority.ToString(), ImGuiTableColumnFlags.WidthFixed, swatchWidth); - } - ImGui.TableHeadersRow(); - - // Create rows for each semantic meaning - be explicit about row count - for (int rowIndex = 0; rowIndex < sortedMeanings.Count; rowIndex++) - { - SemanticMeaning meaning = sortedMeanings[rowIndex]; - ImGui.TableNextRow(); - - // Column 0: Semantic meaning name - ImGui.TableSetColumnIndex(0); - ImGui.Text(meaning.ToString()); - - // Columns 1 to N: Color swatches for each priority - for (int colIndex = 0; colIndex < sortedPriorities.Count; colIndex++) - { - Priority priority = sortedPriorities[colIndex]; - ImGui.TableSetColumnIndex(colIndex + 1); // +1 because column 0 is semantic name - - SemanticColorRequest request = new(meaning, priority); - if (completePalette.TryGetValue(request, out PerceptualColor color)) - { - ImColor imColor = Color.FromPerceptualColor(color); - - // Color swatch button - Vector2 swatchButtonSize = new(swatchWidth, swatchHeight); - if (ImGui.ColorButton($"##swatch_{meaning}_{priority}", - imColor.Value, ImGuiColorEditFlags.None, swatchButtonSize)) - { - // Copy to clipboard on click - string hexColor = $"#{(int)(imColor.Value.X * 255):X2}{(int)(imColor.Value.Y * 255):X2}{(int)(imColor.Value.Z * 255):X2}"; - ImGui.SetClipboardText(hexColor); - } - - // Add icon overlay on all colors using highest priority neutral color - if (neutralIconColor.HasValue) - { - // Draw a test icon (gear/settings icon) over the color to show contrast - Vector2 swatchMin = ImGui.GetItemRectMin(); - Vector2 swatchMax = ImGui.GetItemRectMax(); - Vector2 iconPos = new( - swatchMin.X + ((swatchMax.X - swatchMin.X) * 0.5f), - swatchMin.Y + ((swatchMax.Y - swatchMin.Y) * 0.5f) - ); - - ImDrawListPtr drawList = ImGui.GetWindowDrawList(); - - // Draw a simple gear-like icon using text - string iconText = "⚙"; - Vector2 textSize = ImGui.CalcTextSize(iconText); - Vector2 textPos = new( - iconPos.X - (textSize.X * 0.5f), - iconPos.Y - (textSize.Y * 0.5f) - ); - - // Use the highest priority neutral color for the icon - drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(neutralIconColor.Value.Value), iconText); - } - - // Tooltip - if (ImGui.IsItemHovered()) - { - Vector4 c = imColor.Value; - ImGui.SetTooltip($"Meaning: {meaning}\n" + - $"Priority: {priority}\n" + - $"RGBA: ({c.X:F3}, {c.Y:F3}, {c.Z:F3}, {c.W:F3})\n" + - $"Hex: #{(int)(c.X * 255):X2}{(int)(c.Y * 255):X2}{(int)(c.Z * 255):X2}\n" + - $"Usage: Theme.GetColor(new SemanticColorRequest(SemanticMeaning.{meaning}, Priority.{priority}))\n" + - $"Click to copy to clipboard"); - } - } - else - { - // Empty cell for missing color combinations - ensure we still occupy the cell space - ImGui.Dummy(new Vector2(swatchWidth, swatchHeight)); - } - } - } - - ImGui.EndTable(); - } - ImGui.EndGroup(); - } - - private static void ShowColorPalettesDemo() - { - ImGui.TextUnformatted("🎨 Color Palette System"); - ImGui.Text("Comprehensive color palette with theme-aware colors."); - ImGui.Separator(); - - // Basic Colors Palette - RenderColorPaletteSection("Basic Colors", [ - ("Red", Color.Palette.Basic.Red), - ("Green", Color.Palette.Basic.Green), - ("Blue", Color.Palette.Basic.Blue), - ("Yellow", Color.Palette.Basic.Yellow), - ("Cyan", Color.Palette.Basic.Cyan), - ("Magenta", Color.Palette.Basic.Magenta), - ("Orange", Color.Palette.Basic.Orange), - ("Pink", Color.Palette.Basic.Pink), - ("Lime", Color.Palette.Basic.Lime), - ("Purple", Color.Palette.Basic.Purple), - ]); - - // Semantic Colors Palette - RenderColorPaletteSection("Semantic Colors", [ - ("Error", Color.Palette.Semantic.Error), - ("Warning", Color.Palette.Semantic.Warning), - ("Success", Color.Palette.Semantic.Success), - ("Info", Color.Palette.Semantic.Info), - ("Primary", Color.Palette.Semantic.Primary), - ("Secondary", Color.Palette.Semantic.Secondary), - ]); - - // Neutral Colors Palette - RenderColorPaletteSection("Neutral Colors", [ - ("White", Color.Palette.Neutral.White), - ("Light Gray", Color.Palette.Neutral.LightGray), - ("Gray", Color.Palette.Neutral.Gray), - ("Dark Gray", Color.Palette.Neutral.DarkGray), - ("Black", Color.Palette.Neutral.Black), - ]); - - // Natural Colors Palette - RenderColorPaletteSection("Natural Colors", [ - ("Brown", Color.Palette.Natural.Brown), - ("Olive", Color.Palette.Natural.Olive), - ("Maroon", Color.Palette.Natural.Maroon), - ("Navy", Color.Palette.Natural.Navy), - ("Teal", Color.Palette.Natural.Teal), - ("Indigo", Color.Palette.Natural.Indigo), - ]); - - // Vibrant Colors Palette - RenderColorPaletteSection("Vibrant Colors", [ - ("Coral", Color.Palette.Vibrant.Coral), - ("Salmon", Color.Palette.Vibrant.Salmon), - ("Turquoise", Color.Palette.Vibrant.Turquoise), - ("Violet", Color.Palette.Vibrant.Violet), - ("Gold", Color.Palette.Vibrant.Gold), - ("Silver", Color.Palette.Vibrant.Silver), - ]); - - // Pastel Colors Palette - RenderColorPaletteSection("Pastel Colors", [ - ("Beige", Color.Palette.Pastel.Beige), - ("Peach", Color.Palette.Pastel.Peach), - ("Mint", Color.Palette.Pastel.Mint), - ("Lavender", Color.Palette.Pastel.Lavender), - ("Khaki", Color.Palette.Pastel.Khaki), - ("Plum", Color.Palette.Pastel.Plum), - ]); - - ImGui.Separator(); - - // Interactive color manipulation - ImGui.Text("🔧 Interactive Color Tools:"); - ImGui.ColorEdit4("Custom Color", ref colorValue[0]); - - // Show color variations - ImColor baseColor = Color.FromRGBA(colorValue[0], colorValue[1], colorValue[2], colorValue[3]); - ImGui.Text("Color Variations:"); - - ImGui.BeginGroup(); - RenderColorSwatch("Original", baseColor); - RenderColorSwatch("Darker", baseColor.MultiplyLuminance(0.7f)); - RenderColorSwatch("Lighter", baseColor.MultiplyLuminance(1.3f)); - RenderColorSwatch("Desaturated", baseColor.MultiplySaturation(0.5f)); - RenderColorSwatch("Saturated", baseColor.MultiplySaturation(1.5f)); - ImGui.EndGroup(); - } - - private static void RenderColorPaletteSection(string title, (string Name, ImColor Color)[] colors) - { - ImGui.Text($"â€ĸ {title}:"); - ImGui.BeginGroup(); - - foreach ((string name, ImColor color) in colors) - { - RenderColorSwatch(name, color); - } - - ImGui.EndGroup(); - ImGui.Separator(); - } - - private static void RenderColorSwatch(string name, ImColor color) - { - Vector2 swatchSize = new(30, 25); - ImGui.ColorButton($"##{name}Swatch", color.Value, ImGuiColorEditFlags.None, swatchSize); - - if (ImGui.IsItemHovered()) - { - Vector4 c = color.Value; - ImGui.SetTooltip($"{name}\nRGBA: ({c.X:F2}, {c.Y:F2}, {c.Z:F2}, {c.W:F2})\nHex: #{(int)(c.X * 255):X2}{(int)(c.Y * 255):X2}{(int)(c.Z * 255):X2}"); - } - - ImGui.SameLine(); - ImGui.Text(name); - } - - private static void ShowWidgetShowcase() - { - ImGui.TextUnformatted("đŸ–ąī¸ Comprehensive Widget Showcase"); - ImGui.Text("All ImGui widgets styled with the current theme."); - ImGui.Separator(); - - // Layout in columns for better organization - ImGui.Columns(2, "WidgetColumns", true); - - // Column 1: Input widgets - ImGui.Text("📝 Input Widgets:"); - - ImGui.Button("Standard Button"); - ImGui.SameLine(); - ImGui.SmallButton("Small"); - - if (ImGui.ArrowButton("##left", ImGuiDir.Left)) - { - // Left arrow clicked - } - ImGui.SameLine(); - if (ImGui.ArrowButton("##right", ImGuiDir.Right)) - { - // Right arrow clicked - } - - ImGui.Checkbox("Checkbox", ref valueBool); - ImGui.Checkbox("Checkbox 2", ref valueBool2); - - ImGui.RadioButton("Radio A", ref radioSelection, 0); - ImGui.RadioButton("Radio B", ref radioSelection, 1); - ImGui.RadioButton("Radio C", ref radioSelection, 2); - - ImGui.SliderFloat("Slider", ref valueFloat, 0.0f, 1.0f); - ImGui.SliderFloat("Slider 2", ref valueFloat2, -10.0f, 10.0f); - ImGui.SliderInt("Int Slider", ref valueInt, 0, 100); - - ImGui.InputText("Text Input", ref valueString, 128); - - if (ImGui.BeginCombo("Combo", comboItems[comboSelection])) - { - for (int i = 0; i < comboItems.Length; i++) - { - bool isSelected = comboSelection == i; - if (ImGui.Selectable(comboItems[i], isSelected)) - { - comboSelection = i; - } - if (isSelected) - { - ImGui.SetItemDefaultFocus(); - } - } - ImGui.EndCombo(); - } - - ImGui.ColorEdit3("Color", ref colorValue[0]); - - ImGui.NextColumn(); - - // Column 2: Display widgets - ImGui.Text("📊 Display Widgets:"); - - ImGui.Text($"Current Values:"); - ImGui.BulletText($"Bool: {valueBool}"); - ImGui.BulletText($"Int: {valueInt}"); - ImGui.BulletText($"Float: {valueFloat:F2}"); - ImGui.BulletText($"String: {valueString}"); - - unsafe - { - fixed (float* plotPtr = plotData) - { - ImGui.PlotLines("##Plot"u8, plotPtr, plotData.Length, 0, "Sample Plot"u8, 0.0f, 1.0f, new Vector2(0, 80)); - } - } - - ImGui.ProgressBar(valueFloat, new Vector2(-1, 0), $"{valueFloat * 100:F0}%"); - - // Separator - ImGui.Separator(); - - // Tree node - if (ImGui.TreeNode("Tree Node"u8)) - { - ImGui.Text("Tree content"); - if (ImGui.TreeNode("Nested Node"u8)) - { - ImGui.Text("Nested content"); - ImGui.TreePop(); - } - ImGui.TreePop(); - } - - // Collapsing header - if (ImGui.CollapsingHeader("Collapsing Header"u8)) - { - ImGui.Text("Header content"); - ImGui.Indent(); - ImGui.Text("Indented text"); - ImGui.Unindent(); - } - - ImGui.Columns(1); - - // Full-width section - ImGui.Separator(); - ImGui.Text("📋 Tables & Lists:"); - - if (ImGui.BeginTable("DemoTable"u8, 3, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable | ImGuiTableFlags.RowBg)) - { - ImGui.TableSetupColumn("Column A"u8); - ImGui.TableSetupColumn("Column B"u8); - ImGui.TableSetupColumn("Column C"u8); - ImGui.TableHeadersRow(); - - for (int row = 0; row < 5; row++) - { - ImGui.TableNextRow(); - for (int col = 0; col < 3; col++) - { - ImGui.TableSetColumnIndex(col); - ImGui.Text($"Row {row}, Col {col}"); - } - } - ImGui.EndTable(); - } - } - - private static void ShowInteractiveExamples() - { - ImGui.TextUnformatted("💡 Interactive Theme Examples"); - ImGui.Text("See how themes affect real UI components and workflows."); - ImGui.Separator(); - - // Theme-aware text colors demonstration - ImGui.Text("🎨 Semantic Text Colors in Action:"); - ImGui.BeginChild("TextColorDemo", new Vector2(0, 120), ImGuiChildFlags.Borders); - - using (Text.Color.Error()) - { - ImGui.TextUnformatted("❌ Error: Connection failed!"); - } - - using (Text.Color.Warning()) - { - ImGui.TextUnformatted("âš ī¸ Warning: Low disk space"); - } - - using (Text.Color.Success()) - { - ImGui.TextUnformatted("✅ Success: File saved successfully"); - } - - using (Text.Color.Info()) - { - ImGui.TextUnformatted("â„šī¸ Info: 5 items processed"); - } - - ImGui.EndChild(); - - ImGui.Separator(); - - // Scoped theming demonstration - ImGui.Text("đŸŽ¯ Scoped Theme Applications:"); - - ImGui.Text("Normal themed section:"); - ImGui.Button("Normal Button"); - ImGui.SliderFloat("Normal Slider", ref valueFloat, 0.0f, 1.0f); - - ImGui.Separator(); - - ImGui.Text("Scoped color themes:"); - using (Theme.FromColor(Color.Palette.Semantic.Error)) - { - ImGui.Text("Error-themed section:"); - ImGui.Button("Danger Button"); - ImGui.ProgressBar(0.8f, new Vector2(-1, 0), "80% Critical"); - } - - using (Theme.FromColor(Color.Palette.Semantic.Success)) - { - ImGui.Text("Success-themed section:"); - ImGui.Button("Success Button"); - ImGui.ProgressBar(0.9f, new Vector2(-1, 0), "90% Complete"); - } - - ImGui.Separator(); - - // Form example with validation - ImGui.Text("📋 Form Example with Validation:"); - ImGui.BeginChild("FormExample", new Vector2(0, 150), ImGuiChildFlags.Borders); - - ImGui.Text("User Registration:"); - ImGui.InputText("Username", ref formUsername, 64); - - if (string.IsNullOrWhiteSpace(formUsername)) - { - using ScopedColor errorText = new(Color.Palette.Basic.Red); - ImGui.Text("❌ Required"); - } - else if (formUsername.Length < 3) - { - using ScopedColor warningText = new(Color.Palette.Basic.Yellow); - ImGui.Text("⚠ Username should be at least 3 characters"); - } - else - { - using ScopedColor successText = new(Color.Palette.Basic.Green); - ImGui.Text("✓ Username looks good"); - } - - ImGui.InputText("Email", ref formEmail, 128); - - bool validEmail = formEmail.Contains('@') && formEmail.Contains('.'); - if (!string.IsNullOrWhiteSpace(formEmail) && !validEmail) - { - using ScopedColor errorText = new(Color.Palette.Basic.Red); - ImGui.Text("⚠ Invalid email format"); - } - else if (validEmail) - { - using ScopedColor successText = new(Color.Palette.Basic.Green); - ImGui.Text("✓ Email looks valid"); - } - - bool canSubmit = !string.IsNullOrWhiteSpace(formUsername) && formUsername.Length >= 3 && validEmail; - - if (!canSubmit) - { - using (Theme.DisabledFromColor(Color.Palette.Neutral.Gray)) - { - ImGui.Button("Submit (Complete form first)"); - } - } - else - { - using (Theme.FromColor(Color.Palette.Semantic.Success)) - { - if (ImGui.Button("Submit Registration")) - { - // Handle submission - } - } - } - - ImGui.EndChild(); - - // Form validation example with basic colors - ImGui.Text("📝 Form with Validation:"); - ImGui.InputText("Username", ref formUsername, 64); - ImGui.SameLine(); - if (string.IsNullOrWhiteSpace(formUsername)) - { - using ScopedColor errorText = new(Color.Palette.Basic.Red); - ImGui.Text("❌ Required"); - } - else - { - using ScopedColor successText = new(Color.Palette.Basic.Green); - ImGui.Text("✅ Valid"); - } - - ImGui.InputText("Email", ref formEmail, 64); - ImGui.SameLine(); - if (string.IsNullOrWhiteSpace(formEmail) || !formEmail.Contains('@')) - { - using ScopedColor errorText = new(Color.Palette.Basic.Red); - ImGui.Text("❌ Invalid"); - } - else - { - using ScopedColor successText = new(Color.Palette.Basic.Green); - ImGui.Text("✅ Valid"); - } - - ImGui.Separator(); - - // Scoped Theme example - ImGui.Text("🎨 Scoped Theme Example:"); - ImGui.TextWrapped("The ScopedTheme class applies a complete semantic theme temporarily within a 'using' block, then automatically reverts to the original styling when the scope ends."); - - ImGui.Separator(); - ImGui.Text("Normal styling here..."); - - if (ImGui.Button("Normal Button")) - { - // Normal button styling - } - - // Apply a temporary theme for this section - if (availableThemes.Count > 0) - { - ThemeRegistry.ThemeInfo demoTheme = availableThemes[0]; // Use first available theme - - ImGui.Separator(); - ImGui.Text($"Section with {demoTheme.Name} theme applied using ScopedTheme:"); - - // This 'using' block applies the theme temporarily - using (new ScopedTheme(demoTheme.CreateInstance())) - { - if (ImGui.Button("Themed Button")) - { - // This button uses the scoped theme - } - - ImGui.SameLine(); - if (ImGui.SmallButton("Small Themed")) - { - // This button also uses the scoped theme - } - - ImGui.Checkbox("Themed Checkbox", ref valueBool2); - ImGui.SliderFloat("Themed Slider", ref valueFloat2, 0.0f, 1.0f); - - ImGui.Text("All UI elements in this block use the scoped theme colors!"); - } - // Theme automatically reverts here when the 'using' block ends - } - - ImGui.Separator(); - ImGui.Text("Back to normal styling automatically..."); - if (ImGui.Button("Normal Button Again")) - { - // Back to normal styling - } - - ImGui.TextWrapped("💡 Usage: using (new ScopedTheme(myTheme)) { /* themed UI here */ }"); - - ImGui.Separator(); - } - - private void OnAppMenu() - { - // Use the library's improved theme selector menu - if (Theme.RenderThemeSelectorMenu()) - { - // Theme changed - this is where you would save the current theme to settings - // For example: Settings.Theme = Theme.CurrentThemeName; - if (Theme.CurrentThemeName is null) - { - Console.WriteLine("Theme reset to default"); - currentSelectedTheme = null; // Update our local selection - } - else - { - Console.WriteLine($"Theme changed to: {Theme.CurrentThemeName}"); - // Find and store the corresponding theme info - currentSelectedTheme = availableThemes.FirstOrDefault(t => t.Name == Theme.CurrentThemeName); - } - } - - if (ImGui.BeginMenu("Help")) - { - if (ImGui.MenuItem("About ImGuiStyler")) - { - // Show about dialog - placeholder for demonstration - } - ImGui.EndMenu(); - } - } - - private static void ShowDocumentationDemo() - { - ImGui.TextUnformatted("📚 ImGuiStyler Documentation & Examples"); - ImGui.Separator(); - - ImGui.TextWrapped("ImGuiStyler provides comprehensive theming and styling capabilities for Dear ImGui applications. It integrates with ThemeProvider to offer semantic theming with consistent color meanings across different themes."); - - ImGui.Separator(); - ImGui.Text("Usage Examples:"); - - ImGui.BeginChild("CodeExamples", new Vector2(0, 0), ImGuiChildFlags.Borders); - - ImGui.Text("// Apply semantic themes using ScopedTheme (recommended)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "var theme = ThemeRegistry.FindTheme(\"Dracula\");"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "using (new ScopedTheme(theme.CreateInstance()))"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // All UI rendering in this block uses the theme"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // Color mappings are cached for performance"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " ImGui.Button(\"Themed Button\");"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Clear cache if needed (rarely required)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "ScopedTheme.ClearCache();"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Or apply themes globally (affects all subsequent UI)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.Apply(\"Nord\");"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.Apply(\"Catppuccin Mocha\");"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Reset to default ImGui styling"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.ResetToDefault();"); - ImGui.Text("// or via property"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.CurrentThemeName = null;"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Render theme selection menu in your main menu bar"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (Theme.RenderThemeSelectorMenu())"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // Theme was changed - save current theme to settings"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " Settings.Theme = Theme.CurrentThemeName;"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Render the theme browser modal (call in main render loop)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (Theme.RenderThemeSelector()) // Returns true if theme changed"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // Theme was changed via modal - respond to change"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " Settings.Theme = Theme.CurrentThemeName;"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Programmatically open the theme browser modal"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.ShowThemeSelector(); // Opens the modal dialog"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.ShowThemeSelector(\"Custom Title\", new Vector2(900, 700));"); - ImGui.TextUnformatted(""); - - ImGui.Text("// The ThemeBrowser uses ktsu.ImGuiPopups for proper modal behavior"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "// - Blocks interaction with underlying UI"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "// - ESC to close, centered positioning"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "// - Follows established ktsu.dev modal patterns"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Restore theme on application start"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "Theme.CurrentThemeName = Settings.Theme; // null restores default"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Use semantic text colors"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "using (Text.Color.Error())"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " ImGui.Text(\"Error message\");"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Render theme preview cards"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (ThemeCard.Render(theme))"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " Theme.Apply(theme.Name);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Render a grid of theme cards with delegate callback (recommended)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "ThemeCard.RenderGrid(themes, selectedTheme =>"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // Handle theme selection via delegate"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " currentSelectedTheme = selectedTheme;"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "});"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Or use the return value approach (still supported)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "var clicked = ThemeCard.RenderGrid(themes);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (clicked != null)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " Theme.Apply(clicked.Name);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Use color palette (theme-aware)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "ImColor primaryColor = Color.Palette.Semantic.Primary;"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "ImColor errorColor = Color.Palette.Semantic.Error;"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "ImColor customRed = Color.Palette.Basic.Red; // Adapts to theme"); - ImGui.TextUnformatted(""); - - ImGui.Text("// NEW: Use complete theme palette API (powered by MakeCompletePalette)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "var palette = Theme.GetCurrentThemeCompletePalette();"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "var primaryHigh = new SemanticColorRequest(SemanticMeaning.Primary, Priority.High);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (palette?.TryGetValue(primaryHigh, out var color) == true)"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " ImColor myColor = Color.FromPerceptualColor(color);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Or use the simpler helper methods"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "var request = new SemanticColorRequest(SemanticMeaning.Error, Priority.VeryHigh);"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "if (Theme.TryGetColor(request, out var errorColor))"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " // Use the specific error color from theme"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - ImGui.TextUnformatted(""); - - ImGui.Text("// Scoped theme colors for UI sections"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "using (Theme.FromColor(Color.Palette.Semantic.Success))"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "{"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), " ImGui.Button(\"Success Button\");"); - ImGui.TextColored(new Vector4(0.6f, 0.8f, 0.6f, 1.0f), "}"); - - ImGui.EndChild(); - } - - private void OnMoveOrResize() - { - // Handle window resize if needed - } -} diff --git a/examples/ImGuiStylerDemo/ImGuiStylerDemo.csproj b/examples/ImGuiStylerDemo/ImGuiStylerDemo.csproj deleted file mode 100644 index c1e0a6a..0000000 --- a/examples/ImGuiStylerDemo/ImGuiStylerDemo.csproj +++ /dev/null @@ -1,16 +0,0 @@ -īģŋ - - - - - - net9.0 - - true - - - - - - - diff --git a/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.cs b/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.cs deleted file mode 100644 index f7009d7..0000000 --- a/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.cs +++ /dev/null @@ -1,834 +0,0 @@ -// Copyright (c) ktsu.dev -// All rights reserved. -// Licensed under the MIT license. - -namespace ktsu.ImGui.Examples.Widgets; - -using System.Collections.ObjectModel; -using System.Numerics; -using Hexa.NET.ImGui; -using ktsu.ImGui.App; -using ktsu.ImGui.Popups; -using ktsu.ImGui.Styler; -using ktsu.ImGui.Widgets; -using ktsu.Semantics.Paths; -using ktsu.Semantics.Strings; -using ktsu.TextFilter; - -/// -/// Demo strong string example. -/// -public sealed record class StrongStringExample : SemanticString { } - -/// -/// Demo enum values. -/// -public enum EnumValues -{ - /// - /// First enum value. - /// - Value1, - /// - /// Second enum value. - /// - ValueB, - /// - /// Third enum value. - /// - ValueIII, -} - -internal static class ImGuiWidgetsDemo -{ - private static void Main() - { - ImGuiApp.Start(new() - { - Title = "ImGuiWidgets - Complete Library Demo", - OnStart = OnStart, - OnAppMenu = OnAppMenu, - OnMoveOrResize = OnMoveOrResize, - OnRender = OnRender, - }); - } - - private static float value = 0.5f; - private static float tab2Value = 0.5f; - - private static ImGuiWidgets.DividerContainer DividerContainer { get; } = new("DemoDividerContainer"); - private static ImGuiPopups.MessageOK MessageOK { get; } = new(); - private static ImGuiWidgets.TabPanel DemoTabPanel { get; } = new("DemoTabPanel", true, true); - private static Dictionary TabIds { get; } = []; - private static int NextDynamicTabId { get; set; } = 1; - - private static List GridStrings { get; } = []; - private static int InitialGridItemCount { get; } = 32; - private static int GridItemsToShow { get; set; } = InitialGridItemCount; - private static float GridHeight { get; set; } = 500f; - private static ImGuiWidgets.GridOrder GridOrder { get; set; } = ImGuiWidgets.GridOrder.RowMajor; - private static ImGuiWidgets.IconAlignment GridIconAlignment { get; set; } = ImGuiWidgets.IconAlignment.Vertical; - private static bool GridIconSizeBig { get; set; } = true; - private static bool GridIconCenterWithinCell { get; set; } = true; - private static bool GridFitToContents { get; set; } - private static EnumValues selectedEnumValue = EnumValues.Value1; - private static string selectedStringValue = "Hello"; - private static readonly Collection possibleStringValues = ["Hello", "World", "Goodbye"]; - private static StrongStringExample selectedStrongString = "Strong Hello".As(); - private static readonly Collection possibleStrongStringValues = ["Strong Hello".As(), - "Strong World".As(), "Strong Goodbye".As()]; - - // Static fields for SearchBox filter persistence - private static string BasicSearchTerm = string.Empty; - private static TextFilterType BasicFilterType = TextFilterType.Glob; - private static TextFilterMatchOptions BasicMatchOptions = TextFilterMatchOptions.ByWholeString; - - private static string FilteredSearchTerm = string.Empty; - private static TextFilterType FilteredFilterType = TextFilterType.Glob; - private static TextFilterMatchOptions FilteredMatchOptions = TextFilterMatchOptions.ByWholeString; - - private static string RankedSearchTerm = string.Empty; - - private static string GlobSearchTerm = string.Empty; - private static TextFilterType GlobFilterType = TextFilterType.Glob; - private static TextFilterMatchOptions GlobMatchOptions = TextFilterMatchOptions.ByWholeString; - - private static string RegexSearchTerm = string.Empty; - private static TextFilterType RegexFilterType = TextFilterType.Regex; - private static TextFilterMatchOptions RegexMatchOptions = TextFilterMatchOptions.ByWholeString; - -#pragma warning disable CA5394 //Do not use insecure randomness - private static void OnStart() - { - // Create main layout with dedicated demo sections - DividerContainer.Add(new("Widget Demos", 0.6f, ShowWidgetDemos)); - DividerContainer.Add(new("Advanced Demos", 0.4f, ShowAdvancedDemos)); - - // Initialize TabPanel demo - TabIds["tab1"] = DemoTabPanel.AddTab("tab1", "Tab 1", ShowTab1Content); - TabIds["tab2"] = DemoTabPanel.AddTab("tab2", "Tab 2", ShowTab2Content); - TabIds["tab3"] = DemoTabPanel.AddTab("tab3", "Tab 3", ShowTab3Content); - - // Generate test data for grid demos - for (int i = 0; i < InitialGridItemCount; i++) - { - string randomString = $"{i}:"; - int randomAmount = new Random().Next(2, 32); - for (int j = 0; j < randomAmount; j++) - { - randomString += (char)new Random().Next(32, 127); - } - - GridStrings.Add(randomString); - } - } -#pragma warning restore CA5394 //Do not use insecure randomness - - private static void OnRender(float dt) => DividerContainer.Tick(dt); - - private static void OnAppMenu() - { - // Method intentionally left empty. - } - - private static void OnMoveOrResize() - { - // Method intentionally left empty. - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "")] - private static void ShowWidgetDemos(float size) - { - ImGui.TextUnformatted("ImGuiWidgets Library - Comprehensive Demo"); - ImGui.Separator(); - - ShowKnobDemo(); - ShowColorIndicatorDemo(); - ShowComboDemo(); - ShowTextDemo(); - ShowScopedWidgetsDemo(); - ShowTreeDemo(); - } - - private static void ShowAdvancedDemos(float size) - { - AbsoluteFilePath ktsuIconPath = Environment.CurrentDirectory.As() / "ktsu.png".As(); - ImGuiAppTextureInfo ktsuTexture = ImGuiApp.GetOrLoadTexture(ktsuIconPath); - - ImGui.TextUnformatted("Advanced Widget Demos"); - ImGui.Separator(); - - ShowImageAndIconDemo(ktsuTexture); - ShowTabPanelDemo(); - ShowSearchBoxDemo(); - ShowGridDemo(ktsuTexture); - ShowDividerDemo(); - - MessageOK.ShowIfOpen(); - } - - private static void ShowTabPanelDemo() - { - if (ImGui.CollapsingHeader("TabPanel")) - { - ImGui.TextUnformatted("Tabbed interface with dirty state tracking:"); - ImGui.Separator(); - - // Tab Panel controls - ImGui.TextUnformatted("Tab Management:"); - if (ImGui.Button("Mark Active Tab Dirty")) - { - DemoTabPanel.MarkActiveTabDirty(); - } - ImGui.SameLine(); - if (ImGui.Button("Mark Active Tab Clean")) - { - DemoTabPanel.MarkActiveTabClean(); - } - ImGui.SameLine(); - if (ImGui.Button("Add New Tab")) - { - int tabIndex = NextDynamicTabId++; - string tabKey = $"dynamic{tabIndex}"; - string tabId = $"dyntab_{tabIndex}"; - TabIds[tabKey] = DemoTabPanel.AddTab(tabId, $"Extra Tab {tabIndex}", () => ShowDynamicTabContent(tabIndex)); - } - - ImGui.Separator(); - ImGui.TextUnformatted("Features demonstrated:"); - ImGui.BulletText("Closeable tabs (X button)"); - ImGui.BulletText("Dirty state indicators (*)"); - ImGui.BulletText("Dynamic tab addition"); - ImGui.BulletText("Per-tab state management"); - - ImGui.Separator(); - - // Display tab panel - DemoTabPanel.Draw(); - } - } - - private static void ShowSearchBoxDemo() - { - if (ImGui.CollapsingHeader("SearchBox")) - { - ImGui.TextUnformatted("Powerful search functionality with multiple filter types:"); - ImGui.Separator(); - - ImGui.TextUnformatted("Basic SearchBox (UI only):"); - ImGuiWidgets.SearchBox("##BasicSearch", ref BasicSearchTerm, ref BasicFilterType, ref BasicMatchOptions); - ImGui.TextUnformatted($"Search term: '{BasicSearchTerm}' | Type: {BasicFilterType} | Match: {BasicMatchOptions}"); - - ImGui.Separator(); - ImGui.TextUnformatted("SearchBox with Filtering:"); - - // Using the SearchBox that returns filtered results - List filteredResults = [.. ImGuiWidgets.SearchBox( - "##FilteredSearch", - ref FilteredSearchTerm, - GridStrings, - s => s, - ref FilteredFilterType, - ref FilteredMatchOptions)]; - - if (!string.IsNullOrEmpty(FilteredSearchTerm)) - { - ImGui.TextUnformatted($"Results: {filteredResults.Count} matches for '{FilteredSearchTerm}'"); - ImGui.BeginChild("FilteredResults", new Vector2(0, 100), ImGuiChildFlags.Borders); - foreach (string item in filteredResults.Take(20)) - { - ImGui.TextUnformatted($"â€ĸ {item}"); - } - if (filteredResults.Count > 20) - { - ImGui.TextUnformatted($"... and {filteredResults.Count - 20} more"); - } - ImGui.EndChild(); - } - - ImGui.Separator(); - ImGui.TextUnformatted("Ranked SearchBox (Fuzzy Matching):"); - - List rankedResults = [.. ImGuiWidgets.SearchBoxRanked("##RankedSearch", - ref RankedSearchTerm, - GridStrings, - s => s)]; - - if (!string.IsNullOrEmpty(RankedSearchTerm)) - { - ImGui.TextUnformatted($"Fuzzy Results: {rankedResults.Count} matches for '{RankedSearchTerm}'"); - ImGui.BeginChild("RankedResults", new Vector2(0, 100), ImGuiChildFlags.Borders); - foreach (string item in rankedResults.Take(20)) - { - ImGui.TextUnformatted($"â€ĸ {item}"); - } - if (rankedResults.Count > 20) - { - ImGui.TextUnformatted($"... and {rankedResults.Count - 20} more"); - } - ImGui.EndChild(); - } - - ImGui.Separator(); - ImGui.TextUnformatted("Filter Type Comparison:"); - - ImGui.Columns(2, "SearchComparison"); - - ImGui.TextUnformatted("Glob Pattern (*,?):"); - List globResults = [.. ImGuiWidgets.SearchBox("##GlobSearch", - ref GlobSearchTerm, - GridStrings, - s => s, - ref GlobFilterType, - ref GlobMatchOptions)]; - - if (!string.IsNullOrEmpty(GlobSearchTerm)) - { - ImGui.TextUnformatted($"{globResults.Count} matches"); - ImGui.BeginChild("GlobResults", new Vector2(0, 80), ImGuiChildFlags.Borders); - foreach (string item in globResults.Take(10)) - { - ImGui.TextUnformatted($"â€ĸ {item}"); - } - ImGui.EndChild(); - } - else - { - ImGui.TextUnformatted("Try: *1*, ?:*, [0-9]*"); - } - - ImGui.NextColumn(); - - ImGui.TextUnformatted("Regex Pattern:"); - List regexResults = [.. ImGuiWidgets.SearchBox("##RegexSearch", - ref RegexSearchTerm, - GridStrings, - s => s, - ref RegexFilterType, - ref RegexMatchOptions)]; - - if (!string.IsNullOrEmpty(RegexSearchTerm)) - { - ImGui.TextUnformatted($"{regexResults.Count} matches"); - ImGui.BeginChild("RegexResults", new Vector2(0, 80), ImGuiChildFlags.Borders); - foreach (string item in regexResults.Take(10)) - { - ImGui.TextUnformatted($"â€ĸ {item}"); - } - ImGui.EndChild(); - } - else - { - ImGui.TextUnformatted("Try: ^\\d+, [A-Z]+, .*[aeiou].*"); - } - - ImGui.Columns(1); - } - } - - private static void ShowGridDemo(ImGuiAppTextureInfo ktsuTexture) - { - if (ImGui.CollapsingHeader("Grid Layout")) - { - ImGui.TextUnformatted("Flexible grid layouts with automatic sizing:"); - ImGui.Separator(); - - // Grid settings - inline controls - ImGui.TextUnformatted("Grid Configuration:"); - - bool showGridDebug = ImGuiWidgets.EnableGridDebugDraw; - if (ImGui.Checkbox("Show Grid Debug Draw", ref showGridDebug)) - { - ImGuiWidgets.EnableGridDebugDraw = showGridDebug; - } - ImGui.SameLine(); - - bool showIconDebug = ImGuiWidgets.EnableIconDebugDraw; - if (ImGui.Checkbox("Show Icon Debug Draw", ref showIconDebug)) - { - ImGuiWidgets.EnableIconDebugDraw = showIconDebug; - } - - ImGui.Columns(3, "GridSettings"); - - bool gridIconSizeBig = GridIconSizeBig; - if (ImGui.Checkbox("Big Icons", ref gridIconSizeBig)) - { - GridIconSizeBig = gridIconSizeBig; - } - - bool gridIconCenterWithinCell = GridIconCenterWithinCell; - if (ImGui.Checkbox("Center in Cell", ref gridIconCenterWithinCell)) - { - GridIconCenterWithinCell = gridIconCenterWithinCell; - } - - bool gridFitToContents = GridFitToContents; - if (ImGui.Checkbox("Fit to Contents", ref gridFitToContents)) - { - GridFitToContents = gridFitToContents; - } - - ImGui.NextColumn(); - - int gridItemsToShow = GridItemsToShow; - if (ImGui.SliderInt("Items", ref gridItemsToShow, 0, GridStrings.Count)) - { - GridItemsToShow = gridItemsToShow; - } - - ImGuiWidgets.GridOrder gridOrder = GridOrder; - if (ImGuiWidgets.Combo("Order", ref gridOrder)) - { - GridOrder = gridOrder; - } - - ImGui.NextColumn(); - - ImGuiWidgets.IconAlignment gridIconAlignment = GridIconAlignment; - if (ImGuiWidgets.Combo("Icon Layout", ref gridIconAlignment)) - { - GridIconAlignment = gridIconAlignment; - } - - float gridHeight = GridHeight; - if (ImGui.SliderFloat("Height", ref gridHeight, 100f, 800f)) - { - GridHeight = gridHeight; - } - - ImGui.Columns(1); - ImGui.Separator(); - - // Grid display - float iconSizePx = ImGuiApp.EmsToPx(2.5f); - float bigIconSizePx = iconSizePx * 2; - float gridIconSize = GridIconSizeBig ? bigIconSizePx : iconSizePx; - - Vector2 MeasureGridSize(string item) => ImGuiWidgets.CalcIconSize(item, gridIconSize, GridIconAlignment); - void DrawGridCell(string item, Vector2 cellSize, Vector2 itemSize) - { - if (GridIconCenterWithinCell) - { - using (new Alignment.CenterWithin(itemSize, cellSize)) - { - ImGuiWidgets.Icon(item, ktsuTexture.TextureId, gridIconSize, GridIconAlignment); - } - } - else - { - ImGuiWidgets.Icon(item, ktsuTexture.TextureId, gridIconSize, GridIconAlignment); - } - } - - ImGuiWidgets.GridOptions gridOptions = new() - { - GridSize = new Vector2(ImGui.GetContentRegionAvail().X, GridHeight), - FitToContents = GridFitToContents, - }; - - ImGui.TextUnformatted($"Showing {GridItemsToShow} items in {GridOrder} order:"); - - switch (GridOrder) - { - case ImGuiWidgets.GridOrder.RowMajor: - ImGuiWidgets.RowMajorGrid("demoRowMajorGrid", GridStrings.Take(GridItemsToShow), MeasureGridSize, DrawGridCell, gridOptions); - break; - - case ImGuiWidgets.GridOrder.ColumnMajor: - ImGuiWidgets.ColumnMajorGrid("demoColumnMajorGrid", GridStrings.Take(GridItemsToShow), MeasureGridSize, DrawGridCell, gridOptions); - break; - - default: - throw new NotImplementedException(); - } - } - } - - // Individual widget demo methods - private static void ShowKnobDemo() - { - if (ImGui.CollapsingHeader("Knobs")) - { - ImGui.TextUnformatted("All knob variants with interactive controls:"); - ImGui.Separator(); - - // Show all knob variants - ImGui.Columns(3, "KnobColumns"); - - ImGuiWidgets.Knob("Wiper", ref value, 0, 1, 0, null, ImGuiKnobVariant.Wiper); - ImGui.NextColumn(); - ImGuiWidgets.Knob("Wiper Only", ref value, 0, 1, 0, null, ImGuiKnobVariant.WiperOnly); - ImGui.NextColumn(); - ImGuiWidgets.Knob("Wiper Dot", ref value, 0, 1, 0, null, ImGuiKnobVariant.WiperDot); - ImGui.NextColumn(); - - ImGuiWidgets.Knob("Tick", ref value, 0, 1, 0, null, ImGuiKnobVariant.Tick); - ImGui.NextColumn(); - ImGuiWidgets.Knob("Stepped", ref value, 0, 1, 0, null, ImGuiKnobVariant.Stepped); - ImGui.NextColumn(); - ImGuiWidgets.Knob("Space", ref value, 0, 1, 0, null, ImGuiKnobVariant.Space); - - ImGui.Columns(1); - - ImGui.Separator(); - ImGui.TextUnformatted($"Current Value: {value:F3}"); - - if (ImGui.Button("Reset to 0.5")) - { - value = 0.5f; - } - } - } - - private static void ShowColorIndicatorDemo() - { - if (ImGui.CollapsingHeader("Color Indicators")) - { - ImGui.TextUnformatted("Color indicators show enabled/disabled states:"); - ImGui.Separator(); - - ImGui.TextUnformatted("Status Lights:"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Success, true); - ImGui.SameLine(); - ImGui.TextUnformatted("System OK"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Warning, true); - ImGui.SameLine(); - ImGui.TextUnformatted("Warning"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Error, true); - ImGui.SameLine(); - ImGui.TextUnformatted("Error"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Info, true); - ImGui.SameLine(); - ImGui.TextUnformatted("Info"); - - ImGui.Separator(); - ImGui.TextUnformatted("Enabled vs Disabled:"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Success, true); - ImGui.SameLine(); - ImGui.TextUnformatted("Enabled"); - ImGuiWidgets.ColorIndicator(Color.Palette.Semantic.Success, false); - ImGui.SameLine(); - ImGui.TextUnformatted("Disabled"); - } - } - - private static void ShowComboDemo() - { - if (ImGui.CollapsingHeader("Combo Boxes")) - { - ImGui.TextUnformatted("Type-safe combo boxes for enums and collections:"); - ImGui.Separator(); - - ImGuiWidgets.Combo("Enum Combo", ref selectedEnumValue); - ImGui.TextUnformatted($"Selected: {selectedEnumValue}"); - - ImGui.Separator(); - ImGuiWidgets.Combo("String Combo", ref selectedStringValue, possibleStringValues); - ImGui.TextUnformatted($"Selected: {selectedStringValue}"); - - ImGui.Separator(); - ImGuiWidgets.Combo("Strong String Combo", ref selectedStrongString, possibleStrongStringValues); - ImGui.TextUnformatted($"Selected: {selectedStrongString}"); - } - } - - private static void ShowTextDemo() - { - if (ImGui.CollapsingHeader("Text Utilities")) - { - ImGui.TextUnformatted("Enhanced text rendering with alignment and clipping:"); - ImGui.Separator(); - - // Regular text - ImGuiWidgets.Text("Regular text"); - - ImGui.Separator(); - - // Centered text - ImGui.TextUnformatted("Centered text in available space:"); - ImGuiWidgets.TextCentered("This text is centered!"); - - ImGui.Separator(); - - // Text centered within bounds - ImGui.TextUnformatted("Text centered within 200px container:"); - Vector2 containerSize = new(200, 50); - ImGui.GetWindowDrawList().AddRect( - ImGui.GetCursorScreenPos(), - ImGui.GetCursorScreenPos() + containerSize, - ImGui.GetColorU32(ImGuiCol.Border) - ); - ImGuiWidgets.TextCenteredWithin("Centered within bounds", containerSize); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + containerSize.Y); - - ImGui.Separator(); - - // Clipped text - ImGui.TextUnformatted("Text clipping demo (150px width):"); - Vector2 clipSize = new(150, 30); - ImGui.GetWindowDrawList().AddRect( - ImGui.GetCursorScreenPos(), - ImGui.GetCursorScreenPos() + clipSize, - ImGui.GetColorU32(ImGuiCol.Border) - ); - // Demonstrate text clipping by manually truncating long text - string longText = "This is a very long text that will be clipped with ellipsis"; - float textWidth = ImGui.CalcTextSize(longText).X; - string displayText = longText; - if (textWidth > clipSize.X) - { - // Manually clip the text for demo purposes - while (ImGui.CalcTextSize(displayText + "...").X > clipSize.X && displayText.Length > 0) - { - displayText = displayText[..^1]; - } - displayText += "..."; - } - ImGuiWidgets.TextCenteredWithin(displayText, clipSize); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + clipSize.Y); - } - } - - private static void ShowScopedWidgetsDemo() - { - if (ImGui.CollapsingHeader("Scoped Utilities")) - { - ImGui.TextUnformatted("Scoped helpers for ImGui state management:"); - ImGui.Separator(); - - // ScopedDisable demo - ImGui.TextUnformatted("ScopedDisable - disables widgets within scope:"); - using (new ScopedDisable(true)) - { - bool dummyBool = true; - int dummyInt = 0; - string[] items = ["Item 1", "Item 2", "Item 3"]; - - ImGui.Checkbox("Disabled Checkbox", ref dummyBool); - ImGui.Combo("Disabled Combo", ref dummyInt, items, items.Length); - ImGui.Button("Disabled Button"); - } - - ImGui.Separator(); - - // ScopedId demo - ImGui.TextUnformatted("ScopedId - manages ImGui ID stack automatically:"); - for (int i = 0; i < 3; i++) - { - using (new ImGuiWidgets.ScopedId(i)) - { - bool state = false; - ImGui.Checkbox("Same Label", ref state); - } - } - ImGui.TextUnformatted("↑ Three checkboxes with same label using ScopedId"); - } - } - - private static void ShowTreeDemo() - { - if (ImGui.CollapsingHeader("Tree View")) - { - ImGui.TextUnformatted("Hierarchical tree structure with automatic cleanup:"); - ImGui.Separator(); - - using ImGuiWidgets.Tree tree = new(); - for (int i = 0; i < 3; i++) - { - using (tree.Child) - { - ImGui.Button($"Parent Node {i + 1}"); - - using ImGuiWidgets.Tree subtree = new(); - for (int j = 0; j < 2; j++) - { - using (subtree.Child) - { - ImGui.Button($"Child {j + 1}"); - - if (i == 0 && j == 0) // Show deeper nesting for first item - { - using ImGuiWidgets.Tree deepTree = new(); - using (deepTree.Child) - { - ImGui.Button("Grandchild"); - } - } - } - } - } - } - } - } - - private static void ShowImageAndIconDemo(ImGuiAppTextureInfo ktsuTexture) - { - if (ImGui.CollapsingHeader("Images & Icons")) - { - ImGui.TextUnformatted("Interactive images and icons with events:"); - ImGui.Separator(); - - // Image demo with color tinting - ImGui.TextUnformatted("Clickable Image (with alpha-preserved tinting):"); - Vector4 tintColor = new(1.0f, 0.8f, 0.8f, 1.0f); // Light red tint - if (ImGuiWidgets.Image(ktsuTexture.TextureId, new Vector2(64, 64), tintColor)) - { - MessageOK.Open("Image Clicked", "You clicked the tinted image!"); - } - - ImGui.SameLine(); - if (ImGuiWidgets.Image(ktsuTexture.TextureId, new Vector2(64, 64))) // No tint - { - MessageOK.Open("Image Clicked", "You clicked the normal image!"); - } - - ImGui.Separator(); - - // Icon demos - ImGui.TextUnformatted("Interactive Icons:"); - - float iconSize = ImGuiApp.EmsToPx(4.0f); - - ImGuiWidgets.Icon("Click Me", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Vertical, - new ImGuiWidgets.IconOptions() - { - OnClick = () => MessageOK.Open("Click", "Single click detected!") - }); - - ImGui.SameLine(); - ImGuiWidgets.Icon("Double Click", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Vertical, - new ImGuiWidgets.IconOptions() - { - OnDoubleClick = () => MessageOK.Open("Double Click", "Double click detected!") - }); - - ImGui.SameLine(); - ImGuiWidgets.Icon("Right Click", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Vertical, - new ImGuiWidgets.IconOptions() - { - OnContextMenu = () => - { - if (ImGui.MenuItem("Context Item 1")) - { - MessageOK.Open("Menu", "Context Item 1 selected"); - } - - if (ImGui.MenuItem("Context Item 2")) - { - MessageOK.Open("Menu", "Context Item 2 selected"); - } - - ImGui.Separator(); - if (ImGui.MenuItem("Context Item 3")) - { - MessageOK.Open("Menu", "Context Item 3 selected"); - } - }, - }); - - ImGui.SameLine(); - ImGuiWidgets.Icon("Hover Me", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Vertical, - new ImGuiWidgets.IconOptions() - { - Tooltip = "This is a tooltip that appears when you hover over the icon!" - }); - - ImGui.Separator(); - - ImGui.TextUnformatted("Horizontal Layout Icons:"); - ImGuiWidgets.Icon("Horizontal 1", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Horizontal); - ImGuiWidgets.Icon("Horizontal 2", ktsuTexture.TextureId, iconSize, ImGuiWidgets.IconAlignment.Horizontal); - } - } - - private static void ShowDividerDemo() - { - if (ImGui.CollapsingHeader("Divider Container")) - { - ImGui.TextUnformatted("This entire demo uses a DividerContainer!"); - ImGui.TextUnformatted("The resizable divider between 'Widget Demos' and 'Advanced Demos'"); - ImGui.TextUnformatted("is created using ImGuiWidgets.DividerContainer."); - - ImGui.Separator(); - ImGui.TextUnformatted("DividerContainer features:"); - ImGui.BulletText("Resizable panes with drag handle"); - ImGui.BulletText("Persistent sizing ratios"); - ImGui.BulletText("Automatic content management"); - ImGui.BulletText("Nested dividers support"); - } - } - - // Tab content methods - private static void ShowTab1Content() - { - ImGui.TextUnformatted("This is the content of Tab 1"); - - if (ImGui.Button("Edit Content")) - { - DemoTabPanel.MarkTabDirty(TabIds["tab1"]); - } - - if (ImGui.Button("Save Content")) - { - DemoTabPanel.MarkTabClean(TabIds["tab1"]); - } - - ImGui.TextUnformatted("Dirty State: " + (DemoTabPanel.IsTabDirty(TabIds["tab1"]) ? "Modified" : "Unchanged")); - } - - private static void ShowTab2Content() - { - ImGui.TextUnformatted("This is the content of Tab 2"); - - if (ImGui.SliderFloat("Value", ref tab2Value, 0.0f, 1.0f)) - { - // Mark tab as dirty when slider value changes - DemoTabPanel.MarkTabDirty(TabIds["tab2"]); - } - - if (ImGui.Button("Reset")) - { - tab2Value = 0.5f; - DemoTabPanel.MarkTabClean(TabIds["tab2"]); - } - } - - private static void ShowTab3Content() - { - ImGui.TextUnformatted("This is the content of Tab 3"); - ImGui.TextUnformatted("Try clicking 'Mark Active Tab Dirty' button above"); - ImGui.TextUnformatted("to see the dirty indicator (*) appear next to the tab name."); - - if (ImGui.Button("Toggle Dirty State")) - { - if (DemoTabPanel.IsTabDirty(TabIds["tab3"])) - { - DemoTabPanel.MarkTabClean(TabIds["tab3"]); - } - else - { - DemoTabPanel.MarkTabDirty(TabIds["tab3"]); - } - } - } - - private static void ShowDynamicTabContent(int tabIndex) - { - string tabKey = $"dynamic{tabIndex}"; - ImGui.TextUnformatted($"This is a dynamically added tab ({tabIndex})"); - ImGui.TextUnformatted("The (*) indicator shows when content has been modified."); - - if (ImGui.Button("Toggle Dirty State")) - { - if (DemoTabPanel.IsTabDirty(TabIds[tabKey])) - { - DemoTabPanel.MarkTabClean(TabIds[tabKey]); - } - else - { - DemoTabPanel.MarkTabDirty(TabIds[tabKey]); - } - } - } -} diff --git a/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.csproj b/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.csproj deleted file mode 100644 index dd28c44..0000000 --- a/examples/ImGuiWidgetsDemo/ImGuiWidgetsDemo.csproj +++ /dev/null @@ -1,15 +0,0 @@ -īģŋ - - - - - - net9.0 - - - - - - - - diff --git a/examples/ImGuiWidgetsDemo/ktsu.png b/examples/ImGuiWidgetsDemo/ktsu.png deleted file mode 100644 index 916c2a3..0000000 --- a/examples/ImGuiWidgetsDemo/ktsu.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c680b0e20d577e840fb3b41e7fc3a7604b06f279346310755aa252abd1959e9 -size 15183 diff --git a/global.json b/global.json index c95eeeb..af0efba 100644 --- a/global.json +++ b/global.json @@ -4,9 +4,12 @@ "rollForward": "latestFeature" }, "msbuild-sdks": { - "MSTest.Sdk": "3.10.2", - "ktsu.Sdk": "1.75.0", - "ktsu.Sdk.ConsoleApp": "1.75.0", - "ktsu.Sdk.App": "1.75.0" + "ktsu.Sdk": "1.48.0", + "ktsu.Sdk.Lib": "1.48.0", + "ktsu.Sdk.ConsoleApp": "1.48.0", + "ktsu.Sdk.Test": "1.48.0", + "ktsu.Sdk.ImGuiApp": "1.48.0", + "ktsu.Sdk.WinApp": "1.48.0", + "ktsu.Sdk.WinTest": "1.48.0" } } diff --git a/scripts/PSBuild.psm1 b/scripts/PSBuild.psm1 index 9b0af34..2de013a 100644 --- a/scripts/PSBuild.psm1 +++ b/scripts/PSBuild.psm1 @@ -49,8 +49,6 @@ function Get-BuildConfiguration { The GitHub token for API operations. .PARAMETER NuGetApiKey The NuGet API key for package publishing. Optional - if not provided or empty, NuGet publishing will be skipped. - .PARAMETER KtsuPackageKey - The Ktsu package key for package publishing. Optional - if not provided or empty, Ktsu publishing will be skipped. .PARAMETER WorkspacePath The path to the workspace/repository root. .PARAMETER ExpectedOwner @@ -82,9 +80,6 @@ function Get-BuildConfiguration { [Parameter(Mandatory=$false)] [AllowEmptyString()] [string]$NuGetApiKey = "", - [Parameter(Mandatory=$false)] - [AllowEmptyString()] - [string]$KtsuPackageKey = "", [Parameter(Mandatory=$true)] [string]$WorkspacePath, [Parameter(Mandatory=$true)] @@ -167,7 +162,6 @@ function Get-BuildConfiguration { GitHubRepo = $GitHubRepo GithubToken = $GithubToken NuGetApiKey = $NuGetApiKey - KtsuPackageKey = $KtsuPackageKey ExpectedOwner = $ExpectedOwner Version = "1.0.0-pre.0" ReleaseHash = $GitSha @@ -1177,7 +1171,7 @@ function New-Changelog { # Write latest version's changelog to separate file for GitHub releases $latestPath = if ($OutputPath) { Join-Path $OutputPath $LatestChangelogFile } else { $LatestChangelogFile } $latestVersionNotes = $latestVersionNotes.ReplaceLineEndings($script:lineEnding) - + # Truncate release notes if they exceed NuGet's 35,000 character limit $maxLength = 35000 if ($latestVersionNotes.Length -gt $maxLength) { @@ -1188,14 +1182,14 @@ function New-Changelog { $truncatedNotes += $truncationMessage $latestVersionNotes = $truncatedNotes Write-Information "Truncated release notes to $($latestVersionNotes.Length) characters" -Tags "New-Changelog" - + # Final safety check - ensure we never exceed the limit if ($latestVersionNotes.Length -gt $maxLength) { Write-Warning "Truncated release notes still exceed limit ($($latestVersionNotes.Length) > $maxLength). Further truncating..." -Tags "New-Changelog" $latestVersionNotes = $latestVersionNotes.Substring(0, $maxLength - 50) + "... (truncated)" } } - + [System.IO.File]::WriteAllText($latestPath, $latestVersionNotes, [System.Text.UTF8Encoding]::new($false)) | Write-InformationStream -Tags "New-Changelog" Write-Information "Latest version changelog saved to: $latestPath" -Tags "New-Changelog" @@ -1296,7 +1290,7 @@ function Update-ProjectMetadata { "PROJECT_URL.url", "AUTHORS.url" ) - + # Add latest changelog if it exists if (Test-Path $BuildConfiguration.LatestChangelogFile) { $filesToAdd += $BuildConfiguration.LatestChangelogFile @@ -1315,49 +1309,30 @@ function Update-ProjectMetadata { Write-Information "Current commit hash: $currentHash" -Tags "Update-ProjectMetadata" if (-not [string]::IsNullOrWhiteSpace($postStatus)) { - # Only commit and push changes if this is the official repository - if ($BuildConfiguration.IsOfficial) { - # Configure git user before committing - Set-GitIdentity | Write-InformationStream -Tags "Update-ProjectMetadata" - - Write-Information "Committing changes..." -Tags "Update-ProjectMetadata" - "git commit -m `"$CommitMessage`"" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Update-ProjectMetadata" + # Configure git user before committing + Set-GitIdentity | Write-InformationStream -Tags "Update-ProjectMetadata" - Write-Information "Pushing changes..." -Tags "Update-ProjectMetadata" - "git push" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Update-ProjectMetadata" + Write-Information "Committing changes..." -Tags "Update-ProjectMetadata" + "git commit -m `"$CommitMessage`"" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Update-ProjectMetadata" - Write-Information "Getting release hash..." -Tags "Update-ProjectMetadata" - $releaseHash = "git rev-parse HEAD" | Invoke-ExpressionWithLogging - Write-Information "Metadata committed as $releaseHash" -Tags "Update-ProjectMetadata" + Write-Information "Pushing changes..." -Tags "Update-ProjectMetadata" + "git push" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Update-ProjectMetadata" - Write-Information "Metadata update completed successfully with changes" -Tags "Update-ProjectMetadata" - Write-Information "Version: $version" -Tags "Update-ProjectMetadata" - Write-Information "Release Hash: $releaseHash" -Tags "Update-ProjectMetadata" + Write-Information "Getting release hash..." -Tags "Update-ProjectMetadata" + $releaseHash = "git rev-parse HEAD" | Invoke-ExpressionWithLogging + Write-Information "Metadata committed as $releaseHash" -Tags "Update-ProjectMetadata" - return [PSCustomObject]@{ - Success = $true - Error = "" - Data = [PSCustomObject]@{ - Version = $version - ReleaseHash = $releaseHash - HasChanges = $true - } - } - } - else { - Write-Information "Changes detected but not committing (fork repository)" -Tags "Update-ProjectMetadata" - Write-Information "Metadata files generated locally for build purposes" -Tags "Update-ProjectMetadata" - Write-Information "Version: $version" -Tags "Update-ProjectMetadata" - Write-Information "Using current commit hash: $currentHash" -Tags "Update-ProjectMetadata" + Write-Information "Metadata update completed successfully with changes" -Tags "Update-ProjectMetadata" + Write-Information "Version: $version" -Tags "Update-ProjectMetadata" + Write-Information "Release Hash: $releaseHash" -Tags "Update-ProjectMetadata" - return [PSCustomObject]@{ - Success = $true - Error = "" - Data = [PSCustomObject]@{ - Version = $version - ReleaseHash = $currentHash - HasChanges = $false - } + return [PSCustomObject]@{ + Success = $true + Error = "" + Data = [PSCustomObject]@{ + Version = $version + ReleaseHash = $releaseHash + HasChanges = $true } } } @@ -1471,11 +1446,37 @@ function Invoke-DotNetTest { Runs dotnet test with code coverage collection. .DESCRIPTION Runs dotnet test with code coverage collection. + .PARAMETER Configuration + The build configuration to use. + .PARAMETER CoverageOutputPath + The path to output code coverage results. #> + [CmdletBinding()] + param ( + [string]$Configuration = "Release", + [string]$CoverageOutputPath = "coverage" + ) + Write-StepHeader "Running Tests with Coverage" -Tags "Invoke-DotNetTest" + + # Ensure the TestResults directory exists + $testResultsPath = Join-Path $CoverageOutputPath "TestResults" + New-Item -Path $testResultsPath -ItemType Directory -Force | Out-Null + # Run tests with both coverage collection and TRX logging for SonarQube - "dotnet-coverage collect `"dotnet test`" -f xml -o `"coverage.xml`"" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Invoke-DotNetTest" + "dotnet test --configuration $Configuration /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=`"coverage.opencover.xml`" --results-directory `"$testResultsPath`" --logger `"trx;LogFileName=TestResults.trx`"" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Invoke-DotNetTest" Assert-LastExitCode "Tests failed" + + # Find and copy coverage file to expected location for SonarQube + $coverageFiles = @(Get-ChildItem -Path . -Recurse -Filter "coverage.opencover.xml" -ErrorAction SilentlyContinue) + if ($coverageFiles.Count -gt 0) { + $latestCoverageFile = $coverageFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + $targetCoverageFile = Join-Path $CoverageOutputPath "coverage.opencover.xml" + Copy-Item -Path $latestCoverageFile.FullName -Destination $targetCoverageFile -Force + Write-Information "Coverage file copied to: $targetCoverageFile" -Tags "Invoke-DotNetTest" + } else { + Write-Information "Warning: No coverage file found" -Tags "Invoke-DotNetTest" + } } function Invoke-DotNetPack { @@ -1518,12 +1519,12 @@ function Invoke-DotNetPack { # Override PackageReleaseNotes to use LATEST_CHANGELOG.md instead of full CHANGELOG.md # Use PackageReleaseNotesFile property to avoid command line length limits and escaping issues $releaseNotesProperty = "" - + if (Test-Path $LatestChangelogFile) { # Get absolute path to the changelog file for MSBuild $absoluteChangelogPath = (Resolve-Path $LatestChangelogFile).Path Write-Information "Using release notes from file: $absoluteChangelogPath" -Tags "Invoke-DotNetPack" - + # Use PackageReleaseNotesFile property instead of PackageReleaseNotes to avoid command line issues $releaseNotesProperty = "-p:PackageReleaseNotesFile=`"$absoluteChangelogPath`"" Write-Information "Overriding PackageReleaseNotesFile with latest changelog file path" -Tags "Invoke-DotNetPack" @@ -1544,7 +1545,7 @@ function Invoke-DotNetPack { # Get more details about what might have failed Write-Information "Packaging failed with exit code $LASTEXITCODE, trying again with detailed verbosity..." -Tags "Invoke-DotNetPack" "dotnet pack --configuration $Configuration -logger:`"Microsoft.Build.Logging.ConsoleLogger,Microsoft.Build;Summary;ForceNoAlign;ShowTimestamp;ShowCommandLine;Verbosity=detailed`" --no-build --output $OutputPath $releaseNotesProperty" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Invoke-DotNetPack" - + throw "Library packaging failed with exit code $LASTEXITCODE" } @@ -1722,17 +1723,6 @@ function Invoke-NuGetPublish { } else { Write-Information "Skipping NuGet.org publishing - no API key provided" -Tags "Invoke-NuGetPublish" } - - # Only publish to Ktsu.dev if API key is provided - if (-not [string]::IsNullOrWhiteSpace($BuildConfiguration.KtsuPackageKey)) { - Write-StepHeader "Publishing to packages.ktsu.dev" -Tags "Invoke-NuGetPublish" - - # Execute the command and stream output - "dotnet nuget push `"$($BuildConfiguration.PackagePattern)`" --api-key `"$($BuildConfiguration.KtsuPackageKey)`" --source `"https://packages.ktsu.dev/v3/index.json`" --skip-duplicate" | Invoke-ExpressionWithLogging | Write-InformationStream -Tags "Invoke-NuGetPublish" - Assert-LastExitCode "packages.ktsu.dev package publish failed" - } else { - Write-Information "Skipping packages.ktsu.dev publishing - no API key provided" -Tags "Invoke-NuGetPublish" - } } function New-GitHubRelease { @@ -2152,7 +2142,7 @@ function Invoke-BuildWorkflow { # Build and Test Invoke-DotNetRestore | Write-InformationStream -Tags "Invoke-BuildWorkflow" Invoke-DotNetBuild -Configuration $Configuration -BuildArgs $BuildArgs | Write-InformationStream -Tags "Invoke-BuildWorkflow" - Invoke-DotNetTest | Write-InformationStream -Tags "Invoke-BuildWorkflow" + Invoke-DotNetTest -Configuration $Configuration -CoverageOutputPath "coverage" | Write-InformationStream -Tags "Invoke-BuildWorkflow" return [PSCustomObject]@{ Success = $true @@ -2203,8 +2193,8 @@ function Invoke-ReleaseWorkflow { # Create NuGet packages try { - Write-StepHeader "Packaging Libraries" -Tags "Invoke-DotNetPack" - Invoke-DotNetPack -Configuration $Configuration -OutputPath $BuildConfiguration.StagingPath -LatestChangelogFile $BuildConfiguration.LatestChangelogFile | Write-InformationStream -Tags "Invoke-DotNetPack" + Write-StepHeader "Packaging Libraries" -Tags "Invoke-DotNetPack" + Invoke-DotNetPack -Configuration $Configuration -OutputPath $BuildConfiguration.StagingPath -LatestChangelogFile $BuildConfiguration.LatestChangelogFile | Write-InformationStream -Tags "Invoke-DotNetPack" # Add package paths if they exist if (Test-Path $BuildConfiguration.PackagePattern) { @@ -2450,4 +2440,4 @@ $ProgressPreference = 'Ignore' # Get the line ending for the current system $script:lineEnding = Get-GitLineEnding -#endregion +#endregion \ No newline at end of file diff --git a/tests/ImGui.App.Tests/ImGui.App.Tests.csproj b/tests/ImGui.App.Tests/ImGui.App.Tests.csproj deleted file mode 100644 index c14b0a6..0000000 --- a/tests/ImGui.App.Tests/ImGui.App.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - true - net9.0 - - true - true - true - true - ktsu.ImGui.App.Tests - $(RootNamespace) - - - - - - - - - -