├── .cargo └── config.toml ├── .changes ├── config.json └── readme.md ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other.md ├── pull_request_template.md └── workflows │ ├── covector-status.yml │ ├── covector-version-or-publish.yml │ └── rust.yml ├── .gitignore ├── .vscode └── settings.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Cranky.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bacon.toml ├── clippy.toml ├── crates ├── eframe │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── data │ │ └── icon.png │ └── src │ │ ├── epi │ │ ├── icon_data.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── native │ │ ├── app_icon.rs │ │ ├── epi_integration.rs │ │ ├── file_storage.rs │ │ ├── mod.rs │ │ └── run.rs │ │ └── web │ │ ├── app_runner.rs │ │ ├── backend.rs │ │ ├── events.rs │ │ ├── input.rs │ │ ├── mod.rs │ │ ├── panic_handler.rs │ │ ├── screen_reader.rs │ │ ├── storage.rs │ │ ├── text_agent.rs │ │ ├── web_logger.rs │ │ ├── web_painter.rs │ │ ├── web_painter_glow.rs │ │ ├── web_painter_wgpu.rs │ │ └── web_runner.rs ├── egui-winit │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── clipboard.rs │ │ ├── lib.rs │ │ └── window_settings.rs ├── egui_demo_app │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── apps │ │ ├── custom3d_glow.rs │ │ ├── custom3d_wgpu.rs │ │ ├── custom3d_wgpu_shader.wgsl │ │ ├── fractal_clock.rs │ │ ├── http_app.rs │ │ └── mod.rs │ │ ├── backend_panel.rs │ │ ├── frame_history.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── web.rs │ │ └── wrap_app.rs └── egui_glow │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── examples │ └── pure_glow.rs │ └── src │ ├── lib.rs │ ├── misc_util.rs │ ├── painter.rs │ ├── shader │ ├── fragment.glsl │ └── vertex.glsl │ ├── shader_version.rs │ ├── vao.rs │ └── winit.rs ├── deny.toml ├── rust-toolchain └── scripts ├── build_demo_web.sh ├── cargo_deny.sh ├── check.sh ├── clippy_wasm.sh ├── clippy_wasm └── clippy.toml ├── docs.sh ├── find_bloat.sh ├── generate_changelog.py ├── generate_example_screenshots.sh ├── setup_web.sh ├── start_server.sh ├── wasm_bindgen_check.sh └── wasm_size.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work 2 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 3 | # check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility 4 | # we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93 5 | [target.wasm32-unknown-unknown] 6 | rustflags = ["--cfg=web_sys_unstable_apis"] 7 | -------------------------------------------------------------------------------- /.changes/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitSiteUrl": "https://github.com/tauri-apps/egui/", 3 | "pkgManagers": { 4 | "rust": { 5 | "version": true, 6 | "getPublishedVersion": "cargo search ${ pkg.pkg } --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -", 7 | "prepublish": [ 8 | "sudo apt-get update", 9 | "sudo apt-get install -y libgtk-3-dev", 10 | "cargo install cargo-audit --features=fix", 11 | { 12 | "command": "cargo generate-lockfile", 13 | "dryRunCommand": true, 14 | "runFromRoot": true, 15 | "pipe": true 16 | }, 17 | { 18 | "command": "echo '
\n

Cargo Audit

\n\n```'", 19 | "dryRunCommand": true, 20 | "pipe": true 21 | }, 22 | { 23 | "command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }", 24 | "dryRunCommand": true, 25 | "runFromRoot": true, 26 | "pipe": true 27 | }, 28 | { 29 | "command": "echo '```\n\n
\n'", 30 | "dryRunCommand": true, 31 | "pipe": true 32 | } 33 | ], 34 | "publish": [ 35 | { 36 | "command": "cargo package --allow-dirty", 37 | "dryRunCommand": true 38 | }, 39 | { 40 | "command": "echo '
\n

Cargo Publish

\n\n```'", 41 | "dryRunCommand": true, 42 | "pipe": true 43 | }, 44 | { 45 | "command": "echo \"\\`\\`\\`\"", 46 | "dryRunCommand": true, 47 | "pipe": true 48 | }, 49 | { 50 | "command": "cargo publish --no-verify", 51 | "dryRunCommand": "cargo publish --no-verify --dry-run --allow-dirty", 52 | "pipe": true 53 | }, 54 | { 55 | "command": "echo '```\n\n
\n'", 56 | "dryRunCommand": true, 57 | "pipe": true 58 | } 59 | ], 60 | "postpublish": [ 61 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f", 62 | "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f", 63 | "git push --tags -f" 64 | ], 65 | "assets": [ 66 | { 67 | "path": "${ pkg.path }/${ pkg.pkg }-${ pkgFile.version }.crate", 68 | "name": "${ pkg.pkg }-${ pkgFile.version }.crate" 69 | } 70 | ] 71 | } 72 | }, 73 | "packages": { 74 | "egui-tao": { 75 | "path": "./crates/egui-winit", 76 | "manager": "rust" 77 | }, 78 | "egui_glow_tao": { 79 | "path": "./crates/egui_glow", 80 | "manager": "rust" 81 | }, 82 | "eframe_tao": { 83 | "path": "./crates/eframe", 84 | "manager": "rust" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.changes/readme.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ##### via https://github.com/jbolda/covector 4 | 5 | As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend that it represents the overall change for organizational purposes. 6 | 7 | When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. 8 | 9 | Use the following format: 10 | 11 | ```md 12 | --- 13 | "package-a": patch 14 | "package-b": minor 15 | --- 16 | 17 | Change summary goes here 18 | 19 | ``` 20 | 21 | Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. 22 | 23 | Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). 24 | 25 | Given a version number MAJOR.MINOR.PATCH, increment the: 26 | 27 | - MAJOR version when you make incompatible API changes, 28 | - MINOR version when you add functionality in a backwards compatible manner, and 29 | - PATCH version when you make backwards compatible bug fixes. 30 | 31 | Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 21 | 22 | **Describe the bug** 23 | 24 | 25 | **To Reproduce** 26 | Steps to reproduce the behavior: 27 | 1. 28 | 2. 29 | 3. 30 | 4. 31 | 32 | **Expected behavior** 33 | 34 | 35 | **Screenshots** 36 | 37 | 38 | **Desktop (please complete the following information):** 39 | - OS: 40 | - Browser 41 | - Version 42 | 43 | **Smartphone (please complete the following information):** 44 | - Device: 45 | - OS: 46 | - Browser 47 | - Version 48 | 49 | **Additional context** 50 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | 15 | **Is your feature request related to a problem? Please describe.** 16 | 17 | 18 | **Describe the solution you'd like** 19 | 20 | 21 | **Describe alternatives you've considered** 22 | 23 | 24 | **Additional context** 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: For issues that are neither bugs or feature requests 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you are asking a question, use [the egui discussions forum](https://github.com/emilk/egui/discussions/categories/q-a) instead! 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | Closes . 16 | -------------------------------------------------------------------------------- /.github/workflows/covector-status.yml: -------------------------------------------------------------------------------- 1 | name: covector status 2 | on: [pull_request] 3 | 4 | jobs: 5 | covector: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 # required for use of git history 12 | - name: covector status 13 | uses: jbolda/covector/packages/action@covector-v0.7 14 | id: covector 15 | with: 16 | command: "status" 17 | -------------------------------------------------------------------------------- /.github/workflows/covector-version-or-publish.yml: -------------------------------------------------------------------------------- 1 | name: version or publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - '0.21' 7 | 8 | jobs: 9 | version-or-publish: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 65 12 | outputs: 13 | change: ${{ steps.covector.outputs.change }} 14 | commandRan: ${{ steps.covector.outputs.commandRan }} 15 | successfulPublish: ${{ steps.covector.outputs.successfulPublish }} 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: cargo login 22 | run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }} 23 | - name: git config 24 | run: | 25 | git config --global user.name "${{ github.event.pusher.name }}" 26 | git config --global user.email "${{ github.event.pusher.email }}" 27 | - name: covector version or publish (publish when no change files present) 28 | uses: jbolda/covector/packages/action@covector-v0 29 | id: covector 30 | env: 31 | CARGO_AUDIT_OPTIONS: ${{ secrets.CARGO_AUDIT_OPTIONS }} 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | command: "version-or-publish" 35 | createRelease: true 36 | - name: Create Pull Request With Versions Bumped 37 | id: cpr 38 | uses: tauri-apps/create-pull-request@v3 39 | if: steps.covector.outputs.commandRan == 'version' 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | title: "Publish New Versions" 43 | commit-message: "publish new versions" 44 | labels: "version updates" 45 | branch: "release" 46 | body: ${{ steps.covector.outputs.change }} 47 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | env: 6 | # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, 7 | # as well as by the wasm32-backend of the wgpu crate. 8 | # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html 9 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 10 | RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings 11 | RUSTDOCFLAGS: -D warnings 12 | 13 | jobs: 14 | tests: 15 | name: Tests 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | rust_version: [1.65.0, stable, nightly] 20 | platform: 21 | - { target: x86_64-pc-windows-msvc, os: windows-latest } 22 | - { target: i686-pc-windows-msvc, os: windows-latest } 23 | - { 24 | target: x86_64-pc-windows-gnu, 25 | os: windows-latest, 26 | host: -x86_64-pc-windows-gnu, 27 | } 28 | # - { target: i686-unknown-linux-gnu, os: ubuntu-latest } 29 | - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } 30 | - { target: x86_64-apple-darwin, os: macos-latest } 31 | 32 | env: 33 | RUST_BACKTRACE: 1 34 | CARGO_INCREMENTAL: 0 35 | RUSTFLAGS: "-C debuginfo=0" 36 | OPTIONS: ${{ matrix.platform.options }} 37 | CMD: ${{ matrix.platform.cmd }} 38 | FEATURES: ${{ format(',{0}', matrix.platform.features ) }} 39 | RUSTDOCFLAGS: -D warnings 40 | 41 | runs-on: ${{ matrix.platform.os }} 42 | steps: 43 | - uses: actions/checkout@v1 44 | # Used to cache cargo-web 45 | - name: Cache cargo folder 46 | uses: actions/cache@v1 47 | with: 48 | path: ~/.cargo 49 | key: ${{ matrix.platform.target }}-cargo-${{ matrix.rust_version }} 50 | 51 | - uses: hecrj/setup-rust-action@v1 52 | with: 53 | rust-version: ${{ matrix.rust_version }}${{ matrix.platform.host }} 54 | targets: ${{ matrix.platform.target }} 55 | components: clippy 56 | 57 | # We need those for examples. 58 | - name: Install GCC Multilib 59 | if: (matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686') 60 | run: sudo apt-get update && sudo apt-get install gcc-multilib 61 | 62 | - name: Install Gtk (ubuntu only) 63 | if: matrix.platform.os == 'ubuntu-latest' 64 | run: | 65 | sudo apt-get update 66 | sudo apt-get install -y libgtk-3-dev 67 | 68 | - name: Install Core (windows only) 69 | if: matrix.platform.os == 'windows-latest' 70 | run: | 71 | rustup target add ${{ matrix.platform.target }} 72 | 73 | - name: Install cargo-apk 74 | if: contains(matrix.platform.target, 'android') 75 | run: cargo +stable install cargo-apk 76 | 77 | - name: Build tests (eframe_tao) 78 | shell: bash 79 | run: cargo $CMD test -p eframe_tao --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES 80 | - name: Build tests (egui_glow_tao) 81 | shell: bash 82 | run: cargo $CMD test -p egui_glow_tao --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES 83 | - name: Build tests (egui-tao) 84 | shell: bash 85 | run: cargo $CMD test -p egui-tao --no-run --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES 86 | - name: Run tests 87 | shell: bash 88 | if: ( 89 | !contains(matrix.platform.target, 'android') && 90 | !contains(matrix.platform.target, 'ios') && 91 | !contains(matrix.platform.target, 'wasm32')) 92 | run: cargo test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES 93 | 94 | - name: Check documentation 95 | shell: bash 96 | run: cargo doc --no-deps --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES --document-private-items 97 | 98 | # - name: Lint with clippy 99 | # shell: bash 100 | # if: (matrix.rust_version == '1.65.0') && !contains(matrix.platform.options, '--no-default-features') 101 | # run: cargo clippy --workspace --all-targets --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES -- -Dwarnings 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/target 3 | **/target_ra 4 | **/target_wasm 5 | /.*.json 6 | /.vscode 7 | /media/* 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.insertFinalNewline": true, 3 | "editor.formatOnSave": true, 4 | "files.trimTrailingWhitespace": true, 5 | "editor.semanticTokenColorCustomizations": { 6 | "rules": { 7 | "*.unsafe:rust": "#eb5046" 8 | } 9 | }, 10 | "files.exclude": { 11 | "target_ra/**": true, 12 | "target_wasm/**": true, 13 | "target/**": true, 14 | }, 15 | // Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run` 16 | "rust-analyzer.check.overrideCommand": [ 17 | "cargo", 18 | "cranky", 19 | "--target-dir=target_ra", 20 | "--workspace", 21 | "--message-format=json", 22 | "--all-targets", 23 | ], 24 | "rust-analyzer.cargo.buildScripts.overrideCommand": [ 25 | "cargo", 26 | "check", 27 | "--quiet", 28 | "--target-dir=target_ra", 29 | "--workspace", 30 | "--message-format=json", 31 | "--all-targets", 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | This document describes how the crates that make up egui are all connected. 3 | 4 | Also see [`CONTRIBUTING.md`](CONTRIBUTING.md) for what to do before opening a PR. 5 | 6 | 7 | ## Crate overview 8 | The crates in this repository are: `egui, emath, epaint, egui_extras, egui-winit, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`. 9 | 10 | ### `egui`: The main GUI library. 11 | Example code: `if ui.button("Click me").clicked() { … }` 12 | This is the crate where the bulk of the code is at. `egui` depends only on `emath` and `epaint`. 13 | 14 | ### `emath`: minimal 2D math library 15 | Examples: `Vec2, Pos2, Rect, lerp, remap` 16 | 17 | ### `epaint` 18 | 2d shapes and text that can be turned into textured triangles. 19 | 20 | Example: `Shape::Circle { center, radius, fill, stroke }` 21 | 22 | Depends on `emath`. 23 | 24 | ### `egui_extras` 25 | This adds additional features on top of `egui`. 26 | 27 | ### `egui-winit` 28 | This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit). 29 | 30 | The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc. 31 | 32 | ### `egui_glium` 33 | Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glium](https://github.com/glium/glium). 34 | 35 | ### `egui_glow` 36 | Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glow](https://github.com/grovesNL/glow). 37 | 38 | ### `eframe` 39 | `eframe` is the official `egui` framework, built so you can compile the same app for either web or native. 40 | 41 | The demo that you can see at is using `eframe` to host the `egui`. The demo code is found in: 42 | 43 | ### `egui_demo_lib` 44 | Depends on `egui`. 45 | This contains a bunch of uses of `egui` and looks like the ui code you would write for an `egui` app. 46 | 47 | ### `egui_demo_app` 48 | Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a native app executable. 49 | Depends on `egui_demo_lib` + `eframe`. 50 | 51 | ### Other integrations 52 | 53 | There are also many great integrations for game engines such as `bevy` and `miniquad` which you can find at . 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [the egui discord](https://discord.gg/JFcEma9bJq). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## Introduction 4 | 5 | `egui` has been an on-and-off weekend project of mine since late 2018. I am grateful to any help I can get, but bare in mind that sometimes I can be slow to respond because I am busy with other things! 6 | 7 | / Emil 8 | 9 | 10 | ## Discussion 11 | 12 | You can ask questions, share screenshots and more at [GitHub Discussions](https://github.com/emilk/egui/discussions). 13 | 14 | There is an `egui` discord at . 15 | 16 | 17 | ## Filing an issue 18 | 19 | [Issues](https://github.com/emilk/egui/issues) are for bug reports and feature requests. Issues are not for asking questions (use [Discussions](https://github.com/emilk/egui/discussions) or [Discord](https://discord.gg/vbuv9Xan65) for that). 20 | 21 | Always make sure there is not already a similar issue to the one you are creating. 22 | 23 | If you are filing a bug, please provide a way to reproduce it. 24 | 25 | 26 | ## Making a PR 27 | 28 | First file an issue (or find an existing one) and announce that you plan to work on something. That way we will avoid having several people doing double work. Please ask for feedback before you start working on something non-trivial! 29 | 30 | Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pieces connects. 31 | 32 | You can test your code locally by running `./scripts/check.sh`. 33 | 34 | When you have something that works, open a draft PR. You may get some helpful feedback early! 35 | When you feel the PR is ready to go, do a self-review of the code, and then open it for review. 36 | 37 | Please keep pull requests small and focused. 38 | 39 | Don't worry about having many small commits in the PR - they will be squashed to one commit once merged. 40 | 41 | Do not include the `.js` and `.wasm` build artifacts generated for building for web. 42 | `git` is not great at storing large files like these, so we only commit a new web demo after a new egui release. 43 | 44 | 45 | ## Creating an integration for egui 46 | 47 | If you make an integration for `egui` for some engine or renderer, please share it with the world! 48 | I will add a link to it from the `egui` README.md so others can easily find it. 49 | 50 | Read the section on integrations at . 51 | 52 | 53 | ## Testing the web viewer 54 | * Install some tools with `scripts/setup_web.sh` 55 | * Build with `scripts/build_demo_web.sh` 56 | * Host with `scripts/start_server.sh` 57 | * Open 58 | 59 | 60 | ## Code Conventions 61 | Conventions unless otherwise specified: 62 | 63 | * angles are in radians 64 | * `Vec2::X` is right and `Vec2::Y` is down. 65 | * `Pos2::ZERO` is left top. 66 | 67 | While using an immediate mode gui is simple, implementing one is a lot more tricky. There are many subtle corner-case you need to think through. The `egui` source code is a bit messy, partially because it is still evolving. 68 | 69 | * Read some code before writing your own. 70 | * Follow the `egui` code style. 71 | * Add blank lines around all `fn`, `struct`, `enum`, etc. 72 | * `// Comment like this.` and not `//like this`. 73 | * Use `TODO` instead of `FIXME`. 74 | * Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up`. 75 | * Write idiomatic rust. 76 | * Avoid `unsafe`. 77 | * Avoid code that can cause panics. 78 | * Use good names for everything. 79 | * Add docstrings to types, `struct` fields and all `pub fn`. 80 | * Add some example code (doc-tests). 81 | * Before making a function longer, consider adding a helper function. 82 | * If you are only using it in one function, put the `use` statement in that function. This improves locality, making it easier to read and move the code. 83 | * When importing a `trait` to use it's trait methods, do this: `use Trait as _;`. That lets the reader know why you imported it, even though it seems unused. 84 | * Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/). 85 | * Break the above rules when it makes sense. 86 | 87 | 88 | ### Good: 89 | ``` rust 90 | /// The name of the thing. 91 | fn name(&self) -> &str { 92 | &self.name 93 | } 94 | 95 | fn foo(&self) { 96 | // TODO(emilk): implement 97 | } 98 | ``` 99 | 100 | ### Bad: 101 | ``` rust 102 | //some function 103 | fn get_name(&self) -> &str { 104 | &self.name 105 | } 106 | fn foo(&self) { 107 | //FIXME: implement 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/eframe", 5 | "crates/egui_demo_app", 6 | "crates/egui_glow", 7 | "crates/egui-winit", 8 | ] 9 | 10 | [profile.release] 11 | # lto = true # VERY slightly smaller wasm 12 | # opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3` 13 | # opt-level = 1 # very slow and big wasm. Don't do this. 14 | opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'` 15 | # opt-level = 3 # unnecessarily large wasm for no performance gain 16 | 17 | # debug = true # include debug symbols, useful when profiling wasm 18 | 19 | 20 | [profile.dev] 21 | # Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897 22 | # split-debuginfo = "unpacked" # faster debug builds on mac 23 | # opt-level = 1 # Make debug builds run faster 24 | 25 | # Optimize all dependencies even in debug builds (does not affect workspace packages): 26 | [profile.dev.package."*"] 27 | opt-level = 2 28 | 29 | [workspace.dependencies] 30 | thiserror = "1.0.37" 31 | -------------------------------------------------------------------------------- /Cranky.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/ericseppanen/cargo-cranky 2 | # cargo install cargo-cranky && cargo cranky 3 | 4 | deny = ["unsafe_code"] 5 | 6 | warn = [ 7 | "clippy::all", 8 | "clippy::await_holding_lock", 9 | "clippy::bool_to_int_with_if", 10 | "clippy::char_lit_as_u8", 11 | "clippy::checked_conversions", 12 | "clippy::cloned_instead_of_copied", 13 | "clippy::dbg_macro", 14 | "clippy::debug_assert_with_mut_call", 15 | "clippy::derive_partial_eq_without_eq", 16 | "clippy::disallowed_methods", 17 | "clippy::disallowed_script_idents", 18 | "clippy::doc_link_with_quotes", 19 | "clippy::doc_markdown", 20 | "clippy::empty_enum", 21 | "clippy::enum_glob_use", 22 | "clippy::equatable_if_let", 23 | "clippy::exit", 24 | "clippy::expl_impl_clone_on_copy", 25 | "clippy::explicit_deref_methods", 26 | "clippy::explicit_into_iter_loop", 27 | "clippy::explicit_iter_loop", 28 | "clippy::fallible_impl_from", 29 | "clippy::filter_map_next", 30 | "clippy::flat_map_option", 31 | "clippy::float_cmp_const", 32 | "clippy::fn_params_excessive_bools", 33 | "clippy::fn_to_numeric_cast_any", 34 | "clippy::from_iter_instead_of_collect", 35 | "clippy::if_let_mutex", 36 | "clippy::implicit_clone", 37 | "clippy::imprecise_flops", 38 | "clippy::index_refutable_slice", 39 | "clippy::inefficient_to_string", 40 | "clippy::invalid_upcast_comparisons", 41 | "clippy::iter_not_returning_iterator", 42 | "clippy::iter_on_empty_collections", 43 | "clippy::iter_on_single_items", 44 | "clippy::large_digit_groups", 45 | "clippy::large_stack_arrays", 46 | "clippy::large_types_passed_by_value", 47 | "clippy::let_unit_value", 48 | "clippy::linkedlist", 49 | "clippy::lossy_float_literal", 50 | "clippy::macro_use_imports", 51 | "clippy::manual_assert", 52 | "clippy::manual_instant_elapsed", 53 | "clippy::manual_ok_or", 54 | "clippy::manual_string_new", 55 | "clippy::map_err_ignore", 56 | "clippy::map_flatten", 57 | "clippy::map_unwrap_or", 58 | "clippy::match_on_vec_items", 59 | "clippy::match_same_arms", 60 | "clippy::match_wild_err_arm", 61 | "clippy::match_wildcard_for_single_variants", 62 | "clippy::mem_forget", 63 | "clippy::mismatched_target_os", 64 | "clippy::mismatching_type_param_order", 65 | "clippy::missing_enforced_import_renames", 66 | "clippy::missing_errors_doc", 67 | "clippy::missing_safety_doc", 68 | "clippy::mut_mut", 69 | "clippy::mutex_integer", 70 | "clippy::needless_borrow", 71 | "clippy::needless_continue", 72 | "clippy::needless_for_each", 73 | "clippy::needless_pass_by_value", 74 | "clippy::negative_feature_names", 75 | "clippy::nonstandard_macro_braces", 76 | "clippy::option_option", 77 | "clippy::path_buf_push_overwrite", 78 | "clippy::ptr_as_ptr", 79 | "clippy::rc_mutex", 80 | "clippy::ref_option_ref", 81 | "clippy::rest_pat_in_fully_bound_structs", 82 | "clippy::same_functions_in_if_condition", 83 | "clippy::semicolon_if_nothing_returned", 84 | "clippy::single_match_else", 85 | "clippy::str_to_string", 86 | "clippy::string_add_assign", 87 | "clippy::string_add", 88 | "clippy::string_lit_as_bytes", 89 | "clippy::string_to_string", 90 | "clippy::todo", 91 | "clippy::trailing_empty_array", 92 | "clippy::trait_duplication_in_bounds", 93 | "clippy::unimplemented", 94 | "clippy::unnecessary_wraps", 95 | "clippy::unnested_or_patterns", 96 | "clippy::unused_peekable", 97 | "clippy::unused_rounding", 98 | "clippy::unused_self", 99 | "clippy::useless_transmute", 100 | "clippy::verbose_file_reads", 101 | "clippy::zero_sized_map_values", 102 | "elided_lifetimes_in_paths", 103 | "future_incompatible", 104 | "nonstandard_style", 105 | "rust_2018_idioms", 106 | "rust_2021_prelude_collisions", 107 | "rustdoc::missing_crate_level_docs", 108 | "semicolon_in_expressions_from_macros", 109 | "trivial_numeric_casts", 110 | "unused_extern_crates", 111 | "unused_import_braces", 112 | "unused_lifetimes", 113 | ] 114 | 115 | allow = [ 116 | "clippy::manual_range_contains", # This one is just annoying 117 | 118 | # Some of these we should try to put in "warn": 119 | "clippy::type_complexity", 120 | "clippy::undocumented_unsafe_blocks", 121 | "trivial_casts", 122 | "unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 123 | "unused_qualifications", 124 | ] 125 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021 Emil Ernerfeldt 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui 2 | 3 | > egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. 4 | > 5 | > egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. 6 | 7 | [![](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui) 8 | [![Docs.rs](https://docs.rs/egui/badge.svg)](https://docs.rs/egui) 9 | 10 | ``` 11 | [dependencies] 12 | egui = "0.22.0" 13 | ``` 14 | 15 | This repository provides binding for egui to use tao instead. Currently only `glow` backend is supported. 16 | 17 | For more information on how to use egui, please check out [egui repository](https://github.com/emilk/egui) for both [simple examples](https://github.com/emilk/egui/tree/master/examples) and [detailed documents](https://docs.rs/egui). 18 | 19 | ## Who is egui for? 20 | 21 | Quoting from egui repository: 22 | 23 | > [...] if you are writing something interactive in Rust that needs a simple GUI, egui may be for you. 24 | 25 | ## Demo 26 | 27 | Demo app uses [`eframe_tao`](https://github.com/tauri-apps/egui/tree/master/crates/eframe). 28 | 29 | To test the demo app locally, run `cargo run --release -p egui_demo_app`. 30 | 31 | The native backend is [`egui_glow_tao`](https://github.com/tauri-apps/egui/tree/master/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run: 32 | 33 | `sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev` 34 | 35 | On Fedora Rawhide you need to run: 36 | 37 | `dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel gtk3-devel atk fontconfig-devel` 38 | 39 | **NOTE**: This is just for the demo app - egui itself is completely platform agnostic! 40 | -------------------------------------------------------------------------------- /bacon.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the bacon tool 2 | # More info at https://github.com/Canop/bacon 3 | 4 | default_job = "cranky" 5 | 6 | [jobs] 7 | 8 | [jobs.cranky] 9 | command = [ 10 | "cargo", 11 | "cranky", 12 | "--all-targets", 13 | "--all-features", 14 | "--color=always", 15 | ] 16 | need_stdout = false 17 | watch = ["tests", "benches", "examples"] 18 | 19 | [jobs.wasm] 20 | command = [ 21 | "cargo", 22 | "cranky", 23 | "-p=egui_demo_app", 24 | "--lib", 25 | "--target=wasm32-unknown-unknown", 26 | "--target-dir=target_wasm", 27 | "--all-features", 28 | "--color=always", 29 | ] 30 | need_stdout = false 31 | watch = ["tests", "benches", "examples"] 32 | 33 | [jobs.test] 34 | command = ["cargo", "test", "--color=always"] 35 | need_stdout = true 36 | watch = ["tests"] 37 | 38 | [jobs.doc] 39 | command = ["cargo", "doc", "--color=always", "--all-features", "--no-deps"] 40 | need_stdout = false 41 | 42 | # if the doc compiles, then it opens in your browser and bacon switches 43 | # to the previous job 44 | [jobs.doc-open] 45 | command = [ 46 | "cargo", 47 | "doc", 48 | "--color=always", 49 | "--all-features", 50 | "--no-deps", 51 | "--open", 52 | ] 53 | need_stdout = false 54 | on_success = "back" # so that we don't open the browser at each change 55 | 56 | # You can run your application and have the result displayed in bacon, 57 | # *if* it makes sense for this crate. You can run an example the same 58 | # way. Don't forget the `--color always` part or the errors won't be 59 | # properly parsed. 60 | [jobs.run] 61 | command = ["cargo", "run", "--color=always"] 62 | need_stdout = true 63 | 64 | # You may define here keybindings that would be specific to 65 | # a project, for example a shortcut to launch a specific job. 66 | # Shortcuts to internal functions (scrolling, toggling, etc.) 67 | # should go in your personal prefs.toml file instead. 68 | [keybindings] 69 | i = "job:initial" 70 | c = "job:cranky" 71 | w = "job:wasm" 72 | d = "job:doc-open" 73 | t = "job:test" 74 | r = "job:run" 75 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm. 2 | 3 | msrv = "1.65" 4 | 5 | # Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown 6 | doc-valid-idents = [ 7 | # You must also update the same list in the root `clippy.toml`! 8 | "AccessKit", 9 | "..", 10 | ] 11 | -------------------------------------------------------------------------------- /crates/eframe/Cargo.toml: -------------------------------------------------------------------------------- 1 | lib = { } 2 | 3 | [package] 4 | name = "eframe_tao" 5 | version = "0.23.0" 6 | authors = [ "Emil Ernerfeldt " ] 7 | description = "egui framework - write GUI apps that compiles to web and/or natively" 8 | edition = "2021" 9 | rust-version = "1.65" 10 | homepage = "https://github.com/emilk/egui/tree/master/crates/eframe" 11 | license = "MIT OR Apache-2.0" 12 | readme = "README.md" 13 | repository = "https://github.com/emilk/egui/tree/master/crates/eframe" 14 | categories = [ "gui", "game-development" ] 15 | keywords = [ "egui", "gui", "gamedev" ] 16 | include = [ 17 | "../LICENSE-APACHE", 18 | "../LICENSE-MIT", 19 | "**/*.rs", 20 | "Cargo.toml", 21 | "data/icon.png" 22 | ] 23 | 24 | [package.metadata.docs.rs] 25 | all-features = true 26 | targets = [ "x86_64-unknown-linux-gnu", "wasm32-unknown-unknown" ] 27 | 28 | [features] 29 | default = [ "default_fonts", "glow" ] 30 | default_fonts = [ "egui/default_fonts" ] 31 | glow = [ 32 | "dep:glow", 33 | "dep:egui_glow", 34 | "dep:glutin", 35 | "dep:glutin-winit" 36 | ] 37 | persistence = [ 38 | "directories-next", 39 | "egui-winit/serde", 40 | "egui/persistence", 41 | "ron", 42 | "serde" 43 | ] 44 | puffin = [ "dep:puffin", "egui_glow?/puffin" ] 45 | web_screen_reader = [ "tts" ] 46 | __screenshot = [ ] 47 | 48 | [dependencies] 49 | egui = { version = "0.22.0", default-features = false, features = [ "bytemuck", "log" ] } 50 | log = { version = "0.4", features = [ "std" ] } 51 | document-features = { version = "0.2", optional = true } 52 | egui_glow = { package = "egui_glow_tao", version = "0.23.0", path = "../egui_glow", optional = true, default-features = false } 53 | glow = { version = "0.12", optional = true } 54 | ron = { version = "0.8", optional = true, features = [ "integer128" ] } 55 | serde = { version = "1", optional = true, features = [ "derive" ] } 56 | 57 | [dependencies.thiserror] 58 | workspace = true 59 | 60 | [target."cfg(not(target_arch = \"wasm32\"))".dependencies] 61 | egui-winit = { package = "egui-tao", version = "0.23.0", path = "../egui-winit", default-features = false, features = [ "clipboard", "links" ] } 62 | image = { version = "0.24", default-features = false, features = [ "png" ] } 63 | raw-window-handle = { version = "0.5.0" } 64 | winit = { package = "tao", version = "0.19.0" } 65 | directories-next = { version = "2", optional = true } 66 | pollster = { version = "0.3", optional = true } 67 | glutin = { version = "0.30", optional = true } 68 | # glutin-winit = { package = "glutin_tao", version = "0.33.0", optional = true } 69 | glutin-winit = { package = "glutin_tao", version = "0.33.0", git = "https://github.com/tauri-apps/glutin", branch = "0.31", optional = true } 70 | puffin = { version = "0.15", optional = true } 71 | 72 | [target."cfg(any(target_os = \"macos\"))".dependencies] 73 | cocoa = "0.24.1" 74 | objc = "0.2.7" 75 | 76 | [target."cfg(any(target_os = \"windows\"))".dependencies] 77 | winapi = "0.3.9" 78 | 79 | [target."cfg(target_arch = \"wasm32\")".dependencies] 80 | bytemuck = "1.7" 81 | js-sys = "0.3" 82 | percent-encoding = "2.1" 83 | wasm-bindgen = "0.2.86" 84 | wasm-bindgen-futures = "0.4" 85 | web-sys = { version = "0.3.58", features = [ 86 | "BinaryType", 87 | "Blob", 88 | "Clipboard", 89 | "ClipboardEvent", 90 | "CompositionEvent", 91 | "console", 92 | "CssStyleDeclaration", 93 | "DataTransfer", 94 | "DataTransferItem", 95 | "DataTransferItemList", 96 | "Document", 97 | "DomRect", 98 | "DragEvent", 99 | "Element", 100 | "Event", 101 | "EventListener", 102 | "EventTarget", 103 | "ExtSRgb", 104 | "File", 105 | "FileList", 106 | "FocusEvent", 107 | "HtmlCanvasElement", 108 | "HtmlElement", 109 | "HtmlInputElement", 110 | "InputEvent", 111 | "KeyboardEvent", 112 | "Location", 113 | "MediaQueryList", 114 | "MediaQueryListEvent", 115 | "MouseEvent", 116 | "Navigator", 117 | "Performance", 118 | "Storage", 119 | "Touch", 120 | "TouchEvent", 121 | "TouchList", 122 | "WebGl2RenderingContext", 123 | "WebglDebugRendererInfo", 124 | "WebGlRenderingContext", 125 | "WheelEvent", 126 | "Window" 127 | ] } 128 | raw-window-handle = { version = "0.5.2", optional = true } 129 | tts = { version = "0.25", optional = true, default-features = false } 130 | -------------------------------------------------------------------------------- /crates/eframe/README.md: -------------------------------------------------------------------------------- 1 | # eframe_tao: the [`egui`](https://github.com/emilk/egui) framework for tao 2 | 3 | [![Latest version](https://img.shields.io/crates/v/eframe_tao.svg)](https://crates.io/crates/eframe_tao) 4 | [![Documentation](https://docs.rs/eframe_tao/badge.svg)](https://docs.rs/eframe_tao) 5 | ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 6 | ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) 7 | 8 | `eframe_tao` is a modification of `eframe` to utilized `tao` instead of `winit`. 9 | 10 | `eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (cross platform) or be compiled to a web app (using WASM). 11 | 12 | To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples). 13 | To learn how to set up `eframe` for web and native, go to and follow the instructions there! 14 | 15 | There is also a tutorial video at . 16 | 17 | For how to use `egui`, see [the egui docs](https://docs.rs/egui). 18 | 19 | --- 20 | 21 | `eframe` uses [`egui_glow_tao`](https://github.com/tauri-apps/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-tao`](https://github.com/tauri-apps/egui/tree/master/crates/egui-winit). 22 | 23 | To use on Linux, first run: 24 | 25 | ``` 26 | sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 27 | ``` 28 | 29 | You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info. 30 | 31 | ## Alternatives 32 | 33 | You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in . 34 | 35 | ## Problems with running egui on the web 36 | 37 | `eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides. 38 | 39 | - Rendering: Getting pixel-perfect rendering right on the web is very difficult. 40 | - Search: you cannot search an egui web page like you would a normal web page. 41 | - Bringing up an on-screen keyboard on mobile: there is no JS function to do this, so `eframe` fakes it by adding some invisible DOM elements. It doesn't always work. 42 | - Mobile text editing is not as good as for a normal web app. 43 | - Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns). 44 | - No integration with browser settings for colors and fonts. 45 | 46 | In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work). 47 | 48 | The suggested use for `eframe` are for web apps where performance and responsiveness are more important than accessibility and mobile text editing. 49 | 50 | ## Companion crates 51 | 52 | Not all rust crates work when compiled to WASM, but here are some useful crates have been designed to work well both natively and as WASM: 53 | 54 | - Audio: [`cpal`](https://github.com/RustAudio/cpal). 55 | - HTTP client: [`ehttp`](https://github.com/emilk/ehttp) and [`reqwest`](https://github.com/seanmonstar/reqwest). 56 | - Time: [`chrono`](https://github.com/chronotope/chrono). 57 | - WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock). 58 | 59 | ## Name 60 | 61 | The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`frame` is a framework, `egui` is a library). 62 | -------------------------------------------------------------------------------- /crates/eframe/data/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tauri-apps/egui/97efd554bf12e948ac8e51240fd6f177b3d87bb3/crates/eframe/data/icon.png -------------------------------------------------------------------------------- /crates/eframe/src/epi/icon_data.rs: -------------------------------------------------------------------------------- 1 | /// Image data for an application icon. 2 | /// 3 | /// Use a square image, e.g. 256x256 pixels. 4 | /// You can use a transparent background. 5 | #[derive(Clone)] 6 | pub struct IconData { 7 | /// RGBA pixels, with separate/unmultiplied alpha. 8 | pub rgba: Vec, 9 | 10 | /// Image width. This should be a multiple of 4. 11 | pub width: u32, 12 | 13 | /// Image height. This should be a multiple of 4. 14 | pub height: u32, 15 | } 16 | 17 | impl IconData { 18 | /// Convert into [`image::RgbaImage`] 19 | /// 20 | /// # Errors 21 | /// If this is not a valid png. 22 | pub fn try_from_png_bytes(png_bytes: &[u8]) -> Result { 23 | crate::profile_function!(); 24 | let image = image::load_from_memory(png_bytes)?; 25 | Ok(Self::from_image(image)) 26 | } 27 | 28 | fn from_image(image: image::DynamicImage) -> Self { 29 | let image = image.into_rgba8(); 30 | Self { 31 | width: image.width(), 32 | height: image.height(), 33 | rgba: image.into_raw(), 34 | } 35 | } 36 | 37 | /// Convert into [`image::RgbaImage`] 38 | /// 39 | /// # Errors 40 | /// If `width*height != 4 * rgba.len()`, or if the image is too big. 41 | pub fn to_image(&self) -> Result { 42 | crate::profile_function!(); 43 | let Self { 44 | rgba, 45 | width, 46 | height, 47 | } = self.clone(); 48 | image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| "Invalid IconData".to_owned()) 49 | } 50 | 51 | /// Encode as PNG. 52 | /// 53 | /// # Errors 54 | /// The image is invalid, or the PNG encoder failed. 55 | pub fn to_png_bytes(&self) -> Result, String> { 56 | crate::profile_function!(); 57 | let image = self.to_image()?; 58 | let mut png_bytes: Vec = Vec::new(); 59 | image 60 | .write_to( 61 | &mut std::io::Cursor::new(&mut png_bytes), 62 | image::ImageOutputFormat::Png, 63 | ) 64 | .map_err(|err| err.to_string())?; 65 | Ok(png_bytes) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/eframe/src/native/file_storage.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | // ---------------------------------------------------------------------------- 7 | 8 | /// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk. 9 | /// Used to restore egui state, glium window position/size and app state. 10 | pub struct FileStorage { 11 | ron_filepath: PathBuf, 12 | kv: HashMap, 13 | dirty: bool, 14 | last_save_join_handle: Option>, 15 | } 16 | 17 | impl Drop for FileStorage { 18 | fn drop(&mut self) { 19 | if let Some(join_handle) = self.last_save_join_handle.take() { 20 | join_handle.join().ok(); 21 | } 22 | } 23 | } 24 | 25 | impl FileStorage { 26 | /// Store the state in this .ron file. 27 | pub fn from_ron_filepath(ron_filepath: impl Into) -> Self { 28 | let ron_filepath: PathBuf = ron_filepath.into(); 29 | log::debug!("Loading app state from {:?}…", ron_filepath); 30 | Self { 31 | kv: read_ron(&ron_filepath).unwrap_or_default(), 32 | ron_filepath, 33 | dirty: false, 34 | last_save_join_handle: None, 35 | } 36 | } 37 | 38 | /// Find a good place to put the files that the OS likes. 39 | pub fn from_app_name(app_name: &str) -> Option { 40 | if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) { 41 | let data_dir = proj_dirs.data_dir().to_path_buf(); 42 | if let Err(err) = std::fs::create_dir_all(&data_dir) { 43 | log::warn!( 44 | "Saving disabled: Failed to create app path at {:?}: {}", 45 | data_dir, 46 | err 47 | ); 48 | None 49 | } else { 50 | Some(Self::from_ron_filepath(data_dir.join("app.ron"))) 51 | } 52 | } else { 53 | log::warn!("Saving disabled: Failed to find path to data_dir."); 54 | None 55 | } 56 | } 57 | } 58 | 59 | impl crate::Storage for FileStorage { 60 | fn get_string(&self, key: &str) -> Option { 61 | self.kv.get(key).cloned() 62 | } 63 | 64 | fn set_string(&mut self, key: &str, value: String) { 65 | if self.kv.get(key) != Some(&value) { 66 | self.kv.insert(key.to_owned(), value); 67 | self.dirty = true; 68 | } 69 | } 70 | 71 | fn flush(&mut self) { 72 | if self.dirty { 73 | self.dirty = false; 74 | 75 | let file_path = self.ron_filepath.clone(); 76 | let kv = self.kv.clone(); 77 | 78 | if let Some(join_handle) = self.last_save_join_handle.take() { 79 | // wait for previous save to complete. 80 | join_handle.join().ok(); 81 | } 82 | 83 | let join_handle = std::thread::spawn(move || { 84 | let file = std::fs::File::create(&file_path).unwrap(); 85 | let config = Default::default(); 86 | ron::ser::to_writer_pretty(file, &kv, config).unwrap(); 87 | log::trace!("Persisted to {:?}", file_path); 88 | }); 89 | 90 | self.last_save_join_handle = Some(join_handle); 91 | } 92 | } 93 | } 94 | 95 | // ---------------------------------------------------------------------------- 96 | 97 | fn read_ron(ron_path: impl AsRef) -> Option 98 | where 99 | T: serde::de::DeserializeOwned, 100 | { 101 | match std::fs::File::open(ron_path) { 102 | Ok(file) => { 103 | let reader = std::io::BufReader::new(file); 104 | match ron::de::from_reader(reader) { 105 | Ok(value) => Some(value), 106 | Err(err) => { 107 | log::warn!("Failed to parse RON: {}", err); 108 | None 109 | } 110 | } 111 | } 112 | Err(_err) => { 113 | // File probably doesn't exist. That's fine. 114 | None 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/eframe/src/native/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_icon; 2 | pub mod epi_integration; 3 | pub mod run; 4 | 5 | /// File storage which can be used by native backends. 6 | #[cfg(feature = "persistence")] 7 | pub mod file_storage; 8 | -------------------------------------------------------------------------------- /crates/eframe/src/web/backend.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use egui::mutex::Mutex; 4 | 5 | use crate::epi; 6 | 7 | use super::percent_decode; 8 | 9 | // ---------------------------------------------------------------------------- 10 | 11 | /// Data gathered between frames. 12 | #[derive(Default)] 13 | pub struct WebInput { 14 | /// Required because we don't get a position on touched 15 | pub latest_touch_pos: Option, 16 | 17 | /// Required to maintain a stable touch position for multi-touch gestures. 18 | pub latest_touch_pos_id: Option, 19 | 20 | pub raw: egui::RawInput, 21 | } 22 | 23 | impl WebInput { 24 | pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput { 25 | egui::RawInput { 26 | screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)), 27 | pixels_per_point: Some(super::native_pixels_per_point()), // We ALWAYS use the native pixels-per-point 28 | time: Some(super::now_sec()), 29 | ..self.raw.take() 30 | } 31 | } 32 | 33 | pub fn on_web_page_focus_change(&mut self, focused: bool) { 34 | self.raw.modifiers = egui::Modifiers::default(); 35 | self.raw.focused = focused; 36 | self.raw.events.push(egui::Event::WindowFocused(focused)); 37 | self.latest_touch_pos = None; 38 | self.latest_touch_pos_id = None; 39 | } 40 | } 41 | 42 | // ---------------------------------------------------------------------------- 43 | 44 | use std::sync::atomic::Ordering::SeqCst; 45 | 46 | /// Stores when to do the next repaint. 47 | pub struct NeedRepaint(Mutex); 48 | 49 | impl Default for NeedRepaint { 50 | fn default() -> Self { 51 | Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint 52 | } 53 | } 54 | 55 | impl NeedRepaint { 56 | /// Returns the time (in [`now_sec`] scale) when 57 | /// we should next repaint. 58 | pub fn when_to_repaint(&self) -> f64 { 59 | *self.0.lock() 60 | } 61 | 62 | /// Unschedule repainting. 63 | pub fn clear(&self) { 64 | *self.0.lock() = f64::INFINITY; 65 | } 66 | 67 | pub fn repaint_after(&self, num_seconds: f64) { 68 | let mut repaint_time = self.0.lock(); 69 | *repaint_time = repaint_time.min(super::now_sec() + num_seconds); 70 | } 71 | 72 | pub fn repaint_asap(&self) { 73 | *self.0.lock() = f64::NEG_INFINITY; 74 | } 75 | } 76 | 77 | pub struct IsDestroyed(std::sync::atomic::AtomicBool); 78 | 79 | impl Default for IsDestroyed { 80 | fn default() -> Self { 81 | Self(false.into()) 82 | } 83 | } 84 | 85 | impl IsDestroyed { 86 | pub fn fetch(&self) -> bool { 87 | self.0.load(SeqCst) 88 | } 89 | 90 | pub fn set_true(&self) { 91 | self.0.store(true, SeqCst); 92 | } 93 | } 94 | 95 | // ---------------------------------------------------------------------------- 96 | 97 | pub fn user_agent() -> Option { 98 | web_sys::window()?.navigator().user_agent().ok() 99 | } 100 | 101 | pub fn web_location() -> epi::Location { 102 | let location = web_sys::window().unwrap().location(); 103 | 104 | let hash = percent_decode(&location.hash().unwrap_or_default()); 105 | 106 | let query = location 107 | .search() 108 | .unwrap_or_default() 109 | .strip_prefix('?') 110 | .map(percent_decode) 111 | .unwrap_or_default(); 112 | 113 | let query_map = parse_query_map(&query) 114 | .iter() 115 | .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())) 116 | .collect(); 117 | 118 | epi::Location { 119 | url: percent_decode(&location.href().unwrap_or_default()), 120 | protocol: percent_decode(&location.protocol().unwrap_or_default()), 121 | host: percent_decode(&location.host().unwrap_or_default()), 122 | hostname: percent_decode(&location.hostname().unwrap_or_default()), 123 | port: percent_decode(&location.port().unwrap_or_default()), 124 | hash, 125 | query, 126 | query_map, 127 | origin: percent_decode(&location.origin().unwrap_or_default()), 128 | } 129 | } 130 | 131 | fn parse_query_map(query: &str) -> BTreeMap<&str, &str> { 132 | query 133 | .split('&') 134 | .filter_map(|pair| { 135 | if pair.is_empty() { 136 | None 137 | } else { 138 | Some(if let Some((key, value)) = pair.split_once('=') { 139 | (key, value) 140 | } else { 141 | (pair, "") 142 | }) 143 | } 144 | }) 145 | .collect() 146 | } 147 | 148 | #[test] 149 | fn test_parse_query() { 150 | assert_eq!(parse_query_map(""), BTreeMap::default()); 151 | assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")])); 152 | assert_eq!( 153 | parse_query_map("foo=bar"), 154 | BTreeMap::from_iter([("foo", "bar")]) 155 | ); 156 | assert_eq!( 157 | parse_query_map("foo=bar&baz=42"), 158 | BTreeMap::from_iter([("foo", "bar"), ("baz", "42")]) 159 | ); 160 | assert_eq!( 161 | parse_query_map("foo&baz=42"), 162 | BTreeMap::from_iter([("foo", ""), ("baz", "42")]) 163 | ); 164 | assert_eq!( 165 | parse_query_map("foo&baz&&"), 166 | BTreeMap::from_iter([("foo", ""), ("baz", "")]) 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /crates/eframe/src/web/input.rs: -------------------------------------------------------------------------------- 1 | use super::{canvas_element, canvas_origin, AppRunner}; 2 | 3 | pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 { 4 | let canvas = canvas_element(canvas_id).unwrap(); 5 | let rect = canvas.get_bounding_client_rect(); 6 | egui::Pos2 { 7 | x: event.client_x() as f32 - rect.left() as f32, 8 | y: event.client_y() as f32 - rect.top() as f32, 9 | } 10 | } 11 | 12 | pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option { 13 | match event.button() { 14 | 0 => Some(egui::PointerButton::Primary), 15 | 1 => Some(egui::PointerButton::Middle), 16 | 2 => Some(egui::PointerButton::Secondary), 17 | 3 => Some(egui::PointerButton::Extra1), 18 | 4 => Some(egui::PointerButton::Extra2), 19 | _ => None, 20 | } 21 | } 22 | 23 | /// A single touch is translated to a pointer movement. When a second touch is added, the pointer 24 | /// should not jump to a different position. Therefore, we do not calculate the average position 25 | /// of all touches, but we keep using the same touch as long as it is available. 26 | /// 27 | /// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the 28 | /// pointer position. 29 | pub fn pos_from_touch_event( 30 | canvas_id: &str, 31 | event: &web_sys::TouchEvent, 32 | touch_id_for_pos: &mut Option, 33 | ) -> egui::Pos2 { 34 | let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { 35 | // search for the touch we previously used for the position 36 | // (unfortunately, `event.touches()` is not a rust collection): 37 | (0..event.touches().length()) 38 | .into_iter() 39 | .map(|i| event.touches().get(i).unwrap()) 40 | .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) 41 | } else { 42 | None 43 | }; 44 | // Use the touch found above or pick the first, or return a default position if there is no 45 | // touch at all. (The latter is not expected as the current method is only called when there is 46 | // at least one touch.) 47 | touch_for_pos 48 | .or_else(|| event.touches().get(0)) 49 | .map_or(Default::default(), |touch| { 50 | *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); 51 | pos_from_touch(canvas_origin(canvas_id), &touch) 52 | }) 53 | } 54 | 55 | fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { 56 | egui::Pos2 { 57 | x: touch.page_x() as f32 - canvas_origin.x, 58 | y: touch.page_y() as f32 - canvas_origin.y, 59 | } 60 | } 61 | 62 | pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) { 63 | let canvas_origin = canvas_origin(runner.canvas_id()); 64 | for touch_idx in 0..event.changed_touches().length() { 65 | if let Some(touch) = event.changed_touches().item(touch_idx) { 66 | runner.input.raw.events.push(egui::Event::Touch { 67 | device_id: egui::TouchDeviceId(0), 68 | id: egui::TouchId::from(touch.identifier()), 69 | phase, 70 | pos: pos_from_touch(canvas_origin, &touch), 71 | force: touch.force(), 72 | }); 73 | } 74 | } 75 | } 76 | 77 | /// Web sends all keys as strings, so it is up to us to figure out if it is 78 | /// a real text input or the name of a key. 79 | pub fn should_ignore_key(key: &str) -> bool { 80 | let is_function_key = key.starts_with('F') && key.len() > 1; 81 | is_function_key 82 | || matches!( 83 | key, 84 | "Alt" 85 | | "ArrowDown" 86 | | "ArrowLeft" 87 | | "ArrowRight" 88 | | "ArrowUp" 89 | | "Backspace" 90 | | "CapsLock" 91 | | "ContextMenu" 92 | | "Control" 93 | | "Delete" 94 | | "End" 95 | | "Enter" 96 | | "Esc" 97 | | "Escape" 98 | | "GroupNext" // https://github.com/emilk/egui/issues/510 99 | | "Help" 100 | | "Home" 101 | | "Insert" 102 | | "Meta" 103 | | "NumLock" 104 | | "PageDown" 105 | | "PageUp" 106 | | "Pause" 107 | | "ScrollLock" 108 | | "Shift" 109 | | "Tab" 110 | ) 111 | } 112 | 113 | /// Web sends all all keys as strings, so it is up to us to figure out if it is 114 | /// a real text input or the name of a key. 115 | pub fn translate_key(key: &str) -> Option { 116 | use egui::Key; 117 | 118 | match key { 119 | "ArrowDown" => Some(Key::ArrowDown), 120 | "ArrowLeft" => Some(Key::ArrowLeft), 121 | "ArrowRight" => Some(Key::ArrowRight), 122 | "ArrowUp" => Some(Key::ArrowUp), 123 | 124 | "Esc" | "Escape" => Some(Key::Escape), 125 | "Tab" => Some(Key::Tab), 126 | "Backspace" => Some(Key::Backspace), 127 | "Enter" => Some(Key::Enter), 128 | "Space" | " " => Some(Key::Space), 129 | 130 | "Help" | "Insert" => Some(Key::Insert), 131 | "Delete" => Some(Key::Delete), 132 | "Home" => Some(Key::Home), 133 | "End" => Some(Key::End), 134 | "PageUp" => Some(Key::PageUp), 135 | "PageDown" => Some(Key::PageDown), 136 | 137 | "-" => Some(Key::Minus), 138 | "+" | "=" => Some(Key::PlusEquals), 139 | 140 | "0" => Some(Key::Num0), 141 | "1" => Some(Key::Num1), 142 | "2" => Some(Key::Num2), 143 | "3" => Some(Key::Num3), 144 | "4" => Some(Key::Num4), 145 | "5" => Some(Key::Num5), 146 | "6" => Some(Key::Num6), 147 | "7" => Some(Key::Num7), 148 | "8" => Some(Key::Num8), 149 | "9" => Some(Key::Num9), 150 | 151 | "a" | "A" => Some(Key::A), 152 | "b" | "B" => Some(Key::B), 153 | "c" | "C" => Some(Key::C), 154 | "d" | "D" => Some(Key::D), 155 | "e" | "E" => Some(Key::E), 156 | "f" | "F" => Some(Key::F), 157 | "g" | "G" => Some(Key::G), 158 | "h" | "H" => Some(Key::H), 159 | "i" | "I" => Some(Key::I), 160 | "j" | "J" => Some(Key::J), 161 | "k" | "K" => Some(Key::K), 162 | "l" | "L" => Some(Key::L), 163 | "m" | "M" => Some(Key::M), 164 | "n" | "N" => Some(Key::N), 165 | "o" | "O" => Some(Key::O), 166 | "p" | "P" => Some(Key::P), 167 | "q" | "Q" => Some(Key::Q), 168 | "r" | "R" => Some(Key::R), 169 | "s" | "S" => Some(Key::S), 170 | "t" | "T" => Some(Key::T), 171 | "u" | "U" => Some(Key::U), 172 | "v" | "V" => Some(Key::V), 173 | "w" | "W" => Some(Key::W), 174 | "x" | "X" => Some(Key::X), 175 | "y" | "Y" => Some(Key::Y), 176 | "z" | "Z" => Some(Key::Z), 177 | 178 | "F1" => Some(Key::F1), 179 | "F2" => Some(Key::F2), 180 | "F3" => Some(Key::F3), 181 | "F4" => Some(Key::F4), 182 | "F5" => Some(Key::F5), 183 | "F6" => Some(Key::F6), 184 | "F7" => Some(Key::F7), 185 | "F8" => Some(Key::F8), 186 | "F9" => Some(Key::F9), 187 | "F10" => Some(Key::F10), 188 | "F11" => Some(Key::F11), 189 | "F12" => Some(Key::F12), 190 | "F13" => Some(Key::F13), 191 | "F14" => Some(Key::F14), 192 | "F15" => Some(Key::F15), 193 | "F16" => Some(Key::F16), 194 | "F17" => Some(Key::F17), 195 | "F18" => Some(Key::F18), 196 | "F19" => Some(Key::F19), 197 | "F20" => Some(Key::F20), 198 | 199 | _ => None, 200 | } 201 | } 202 | 203 | pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { 204 | egui::Modifiers { 205 | alt: event.alt_key(), 206 | ctrl: event.ctrl_key(), 207 | shift: event.shift_key(), 208 | 209 | // Ideally we should know if we are running or mac or not, 210 | // but this works good enough for now. 211 | mac_cmd: event.meta_key(), 212 | 213 | // Ideally we should know if we are running or mac or not, 214 | // but this works good enough for now. 215 | command: event.ctrl_key() || event.meta_key(), 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /crates/eframe/src/web/mod.rs: -------------------------------------------------------------------------------- 1 | //! [`egui`] bindings for web apps (compiling to WASM). 2 | 3 | #![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>` 4 | 5 | mod app_runner; 6 | pub mod backend; 7 | mod events; 8 | mod input; 9 | mod panic_handler; 10 | pub mod screen_reader; 11 | pub mod storage; 12 | mod text_agent; 13 | mod web_logger; 14 | mod web_runner; 15 | 16 | pub(crate) use app_runner::AppRunner; 17 | pub use panic_handler::{PanicHandler, PanicSummary}; 18 | pub use web_logger::WebLogger; 19 | pub use web_runner::WebRunner; 20 | 21 | #[cfg(not(any(feature = "glow", feature = "wgpu")))] 22 | compile_error!("You must enable either the 'glow' or 'wgpu' feature"); 23 | 24 | mod web_painter; 25 | 26 | #[cfg(feature = "glow")] 27 | mod web_painter_glow; 28 | #[cfg(feature = "glow")] 29 | pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow; 30 | 31 | #[cfg(feature = "wgpu")] 32 | mod web_painter_wgpu; 33 | #[cfg(all(feature = "wgpu", not(feature = "glow")))] 34 | pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; 35 | 36 | pub use backend::*; 37 | pub use events::*; 38 | pub use storage::*; 39 | 40 | use egui::Vec2; 41 | use wasm_bindgen::prelude::*; 42 | use web_sys::MediaQueryList; 43 | 44 | use input::*; 45 | 46 | use crate::Theme; 47 | 48 | // ---------------------------------------------------------------------------- 49 | 50 | /// Current time in seconds (since undefined point in time). 51 | /// 52 | /// Monotonically increasing. 53 | pub fn now_sec() -> f64 { 54 | web_sys::window() 55 | .expect("should have a Window") 56 | .performance() 57 | .expect("should have a Performance") 58 | .now() 59 | / 1000.0 60 | } 61 | 62 | #[allow(dead_code)] 63 | pub fn screen_size_in_native_points() -> Option { 64 | let window = web_sys::window()?; 65 | Some(egui::vec2( 66 | window.inner_width().ok()?.as_f64()? as f32, 67 | window.inner_height().ok()?.as_f64()? as f32, 68 | )) 69 | } 70 | 71 | pub fn native_pixels_per_point() -> f32 { 72 | let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32; 73 | if pixels_per_point > 0.0 && pixels_per_point.is_finite() { 74 | pixels_per_point 75 | } else { 76 | 1.0 77 | } 78 | } 79 | 80 | pub fn system_theme() -> Option { 81 | let dark_mode = prefers_color_scheme_dark(&web_sys::window()?) 82 | .ok()?? 83 | .matches(); 84 | Some(theme_from_dark_mode(dark_mode)) 85 | } 86 | 87 | fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result, JsValue> { 88 | window.match_media("(prefers-color-scheme: dark)") 89 | } 90 | 91 | fn theme_from_dark_mode(dark_mode: bool) -> Theme { 92 | if dark_mode { 93 | Theme::Dark 94 | } else { 95 | Theme::Light 96 | } 97 | } 98 | 99 | pub fn canvas_element(canvas_id: &str) -> Option { 100 | let document = web_sys::window()?.document()?; 101 | let canvas = document.get_element_by_id(canvas_id)?; 102 | canvas.dyn_into::().ok() 103 | } 104 | 105 | pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { 106 | canvas_element(canvas_id) 107 | .unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id)) 108 | } 109 | 110 | fn canvas_origin(canvas_id: &str) -> egui::Pos2 { 111 | let rect = canvas_element(canvas_id) 112 | .unwrap() 113 | .get_bounding_client_rect(); 114 | egui::pos2(rect.left() as f32, rect.top() as f32) 115 | } 116 | 117 | pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { 118 | let canvas = canvas_element(canvas_id).unwrap(); 119 | let pixels_per_point = native_pixels_per_point(); 120 | egui::vec2( 121 | canvas.width() as f32 / pixels_per_point, 122 | canvas.height() as f32 / pixels_per_point, 123 | ) 124 | } 125 | 126 | pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> { 127 | let canvas = canvas_element(canvas_id)?; 128 | let parent = canvas.parent_element()?; 129 | 130 | // Prefer the client width and height so that if the parent 131 | // element is resized that the egui canvas resizes appropriately. 132 | let width = parent.client_width(); 133 | let height = parent.client_height(); 134 | 135 | let canvas_real_size = Vec2 { 136 | x: width as f32, 137 | y: height as f32, 138 | }; 139 | 140 | if width <= 0 || height <= 0 { 141 | log::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height); 142 | } 143 | 144 | let pixels_per_point = native_pixels_per_point(); 145 | 146 | let max_size_pixels = pixels_per_point * max_size_points; 147 | 148 | let canvas_size_pixels = pixels_per_point * canvas_real_size; 149 | let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels); 150 | let canvas_size_points = canvas_size_pixels / pixels_per_point; 151 | 152 | // Make sure that the height and width are always even numbers. 153 | // otherwise, the page renders blurry on some platforms. 154 | // See https://github.com/emilk/egui/issues/103 155 | fn round_to_even(v: f32) -> f32 { 156 | (v / 2.0).round() * 2.0 157 | } 158 | 159 | canvas 160 | .style() 161 | .set_property( 162 | "width", 163 | &format!("{}px", round_to_even(canvas_size_points.x)), 164 | ) 165 | .ok()?; 166 | canvas 167 | .style() 168 | .set_property( 169 | "height", 170 | &format!("{}px", round_to_even(canvas_size_points.y)), 171 | ) 172 | .ok()?; 173 | canvas.set_width(round_to_even(canvas_size_pixels.x) as u32); 174 | canvas.set_height(round_to_even(canvas_size_pixels.y) as u32); 175 | 176 | Some(()) 177 | } 178 | 179 | // ---------------------------------------------------------------------------- 180 | 181 | pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { 182 | let document = web_sys::window()?.document()?; 183 | document 184 | .body()? 185 | .style() 186 | .set_property("cursor", cursor_web_name(cursor)) 187 | .ok() 188 | } 189 | 190 | #[cfg(web_sys_unstable_apis)] 191 | pub fn set_clipboard_text(s: &str) { 192 | if let Some(window) = web_sys::window() { 193 | if let Some(clipboard) = window.navigator().clipboard() { 194 | let promise = clipboard.write_text(s); 195 | let future = wasm_bindgen_futures::JsFuture::from(promise); 196 | let future = async move { 197 | if let Err(err) = future.await { 198 | log::error!("Copy/cut action failed: {err:?}"); 199 | } 200 | }; 201 | wasm_bindgen_futures::spawn_local(future); 202 | } 203 | } 204 | } 205 | 206 | fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str { 207 | match cursor { 208 | egui::CursorIcon::Alias => "alias", 209 | egui::CursorIcon::AllScroll => "all-scroll", 210 | egui::CursorIcon::Cell => "cell", 211 | egui::CursorIcon::ContextMenu => "context-menu", 212 | egui::CursorIcon::Copy => "copy", 213 | egui::CursorIcon::Crosshair => "crosshair", 214 | egui::CursorIcon::Default => "default", 215 | egui::CursorIcon::Grab => "grab", 216 | egui::CursorIcon::Grabbing => "grabbing", 217 | egui::CursorIcon::Help => "help", 218 | egui::CursorIcon::Move => "move", 219 | egui::CursorIcon::NoDrop => "no-drop", 220 | egui::CursorIcon::None => "none", 221 | egui::CursorIcon::NotAllowed => "not-allowed", 222 | egui::CursorIcon::PointingHand => "pointer", 223 | egui::CursorIcon::Progress => "progress", 224 | egui::CursorIcon::ResizeHorizontal => "ew-resize", 225 | egui::CursorIcon::ResizeNeSw => "nesw-resize", 226 | egui::CursorIcon::ResizeNwSe => "nwse-resize", 227 | egui::CursorIcon::ResizeVertical => "ns-resize", 228 | 229 | egui::CursorIcon::ResizeEast => "e-resize", 230 | egui::CursorIcon::ResizeSouthEast => "se-resize", 231 | egui::CursorIcon::ResizeSouth => "s-resize", 232 | egui::CursorIcon::ResizeSouthWest => "sw-resize", 233 | egui::CursorIcon::ResizeWest => "w-resize", 234 | egui::CursorIcon::ResizeNorthWest => "nw-resize", 235 | egui::CursorIcon::ResizeNorth => "n-resize", 236 | egui::CursorIcon::ResizeNorthEast => "ne-resize", 237 | egui::CursorIcon::ResizeColumn => "col-resize", 238 | egui::CursorIcon::ResizeRow => "row-resize", 239 | 240 | egui::CursorIcon::Text => "text", 241 | egui::CursorIcon::VerticalText => "vertical-text", 242 | egui::CursorIcon::Wait => "wait", 243 | egui::CursorIcon::ZoomIn => "zoom-in", 244 | egui::CursorIcon::ZoomOut => "zoom-out", 245 | } 246 | } 247 | 248 | pub fn open_url(url: &str, new_tab: bool) -> Option<()> { 249 | let name = if new_tab { "_blank" } else { "_self" }; 250 | 251 | web_sys::window()? 252 | .open_with_url_and_target(url, name) 253 | .ok()?; 254 | Some(()) 255 | } 256 | 257 | /// e.g. "#fragment" part of "www.example.com/index.html#fragment", 258 | /// 259 | /// Percent decoded 260 | pub fn location_hash() -> String { 261 | percent_decode( 262 | &web_sys::window() 263 | .unwrap() 264 | .location() 265 | .hash() 266 | .unwrap_or_default(), 267 | ) 268 | } 269 | 270 | pub fn percent_decode(s: &str) -> String { 271 | percent_encoding::percent_decode_str(s) 272 | .decode_utf8_lossy() 273 | .to_string() 274 | } 275 | -------------------------------------------------------------------------------- /crates/eframe/src/web/panic_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use egui::mutex::Mutex; 4 | use wasm_bindgen::prelude::*; 5 | 6 | /// Detects panics, logs them using `console.error`, and stores the panics message and callstack. 7 | /// 8 | /// This lets you query `PanicHandler` for the panic message (if any) so you can show it in the HTML. 9 | /// 10 | /// Chep to clone (ref-counted). 11 | #[derive(Clone)] 12 | pub struct PanicHandler(Arc>); 13 | 14 | impl PanicHandler { 15 | /// Install a panic hook. 16 | pub fn install() -> Self { 17 | let handler = Self(Arc::new(Mutex::new(Default::default()))); 18 | 19 | let handler_clone = handler.clone(); 20 | let previous_hook = std::panic::take_hook(); 21 | std::panic::set_hook(Box::new(move |panic_info| { 22 | let summary = PanicSummary::new(panic_info); 23 | 24 | // Log it using console.error 25 | error(format!( 26 | "{}\n\nStack:\n\n{}", 27 | summary.message(), 28 | summary.callstack() 29 | )); 30 | 31 | // Remember the summary: 32 | handler_clone.0.lock().summary = Some(summary); 33 | 34 | // Propagate panic info to the previously registered panic hook 35 | previous_hook(panic_info); 36 | })); 37 | 38 | handler 39 | } 40 | 41 | /// Has there been a panic? 42 | pub fn has_panicked(&self) -> bool { 43 | self.0.lock().summary.is_some() 44 | } 45 | 46 | /// What was the panic message and callstack? 47 | pub fn panic_summary(&self) -> Option { 48 | self.0.lock().summary.clone() 49 | } 50 | } 51 | 52 | #[derive(Clone, Default)] 53 | struct PanicHandlerInner { 54 | summary: Option, 55 | } 56 | 57 | /// Contains a summary about a panics. 58 | /// 59 | /// This is basically a human-readable version of [`std::panic::PanicInfo`] 60 | /// with an added callstack. 61 | #[derive(Clone, Debug)] 62 | pub struct PanicSummary { 63 | message: String, 64 | callstack: String, 65 | } 66 | 67 | impl PanicSummary { 68 | pub fn new(info: &std::panic::PanicInfo<'_>) -> Self { 69 | let message = info.to_string(); 70 | let callstack = Error::new().stack(); 71 | Self { message, callstack } 72 | } 73 | 74 | pub fn message(&self) -> String { 75 | self.message.clone() 76 | } 77 | 78 | pub fn callstack(&self) -> String { 79 | self.callstack.clone() 80 | } 81 | } 82 | 83 | #[wasm_bindgen] 84 | extern "C" { 85 | #[wasm_bindgen(js_namespace = console)] 86 | fn error(msg: String); 87 | 88 | type Error; 89 | 90 | #[wasm_bindgen(constructor)] 91 | fn new() -> Error; 92 | 93 | #[wasm_bindgen(structural, method, getter)] 94 | fn stack(error: &Error) -> String; 95 | } 96 | -------------------------------------------------------------------------------- /crates/eframe/src/web/screen_reader.rs: -------------------------------------------------------------------------------- 1 | pub struct ScreenReader { 2 | #[cfg(feature = "tts")] 3 | tts: Option, 4 | } 5 | 6 | #[cfg(not(feature = "tts"))] 7 | #[allow(clippy::derivable_impls)] // False positive 8 | impl Default for ScreenReader { 9 | fn default() -> Self { 10 | Self {} 11 | } 12 | } 13 | 14 | #[cfg(feature = "tts")] 15 | impl Default for ScreenReader { 16 | fn default() -> Self { 17 | let tts = match tts::Tts::default() { 18 | Ok(screen_reader) => { 19 | log::debug!("Initialized screen reader."); 20 | Some(screen_reader) 21 | } 22 | Err(err) => { 23 | log::warn!("Failed to load screen reader: {}", err); 24 | None 25 | } 26 | }; 27 | Self { tts } 28 | } 29 | } 30 | 31 | impl ScreenReader { 32 | #[cfg(not(feature = "tts"))] 33 | #[allow(clippy::unused_self)] 34 | pub fn speak(&mut self, _text: &str) {} 35 | 36 | #[cfg(feature = "tts")] 37 | pub fn speak(&mut self, text: &str) { 38 | if text.is_empty() { 39 | return; 40 | } 41 | if let Some(tts) = &mut self.tts { 42 | log::debug!("Speaking: {:?}", text); 43 | let interrupt = true; 44 | if let Err(err) = tts.speak(text, interrupt) { 45 | log::warn!("Failed to read: {}", err); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/eframe/src/web/storage.rs: -------------------------------------------------------------------------------- 1 | fn local_storage() -> Option { 2 | web_sys::window()?.local_storage().ok()? 3 | } 4 | 5 | pub fn local_storage_get(key: &str) -> Option { 6 | local_storage().map(|storage| storage.get_item(key).ok())?? 7 | } 8 | 9 | pub fn local_storage_set(key: &str, value: &str) { 10 | local_storage().map(|storage| storage.set_item(key, value)); 11 | } 12 | 13 | #[cfg(feature = "persistence")] 14 | pub fn load_memory(ctx: &egui::Context) { 15 | if let Some(memory_string) = local_storage_get("egui_memory_ron") { 16 | match ron::from_str(&memory_string) { 17 | Ok(memory) => { 18 | ctx.memory_mut(|m| *m = memory); 19 | } 20 | Err(err) => { 21 | log::warn!("Failed to parse memory RON: {err}"); 22 | } 23 | } 24 | } 25 | } 26 | 27 | #[cfg(not(feature = "persistence"))] 28 | pub fn load_memory(_: &egui::Context) {} 29 | 30 | #[cfg(feature = "persistence")] 31 | pub fn save_memory(ctx: &egui::Context) { 32 | match ctx.memory(|mem| ron::to_string(mem)) { 33 | Ok(ron) => { 34 | local_storage_set("egui_memory_ron", &ron); 35 | } 36 | Err(err) => { 37 | log::warn!("Failed to serialize memory as RON: {err}"); 38 | } 39 | } 40 | } 41 | 42 | #[cfg(not(feature = "persistence"))] 43 | pub fn save_memory(_: &egui::Context) {} 44 | -------------------------------------------------------------------------------- /crates/eframe/src/web/text_agent.rs: -------------------------------------------------------------------------------- 1 | //! The text agent is an `` element used to trigger 2 | //! mobile keyboard and IME input. 3 | //! 4 | use std::{cell::Cell, rc::Rc}; 5 | 6 | use wasm_bindgen::prelude::*; 7 | 8 | use super::{canvas_element, AppRunner, WebRunner}; 9 | 10 | static AGENT_ID: &str = "egui_text_agent"; 11 | 12 | pub fn text_agent() -> web_sys::HtmlInputElement { 13 | web_sys::window() 14 | .unwrap() 15 | .document() 16 | .unwrap() 17 | .get_element_by_id(AGENT_ID) 18 | .unwrap() 19 | .dyn_into() 20 | .unwrap() 21 | } 22 | 23 | /// Text event handler, 24 | pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { 25 | let window = web_sys::window().unwrap(); 26 | let document = window.document().unwrap(); 27 | let body = document.body().expect("document should have a body"); 28 | let input = document 29 | .create_element("input")? 30 | .dyn_into::()?; 31 | let input = std::rc::Rc::new(input); 32 | input.set_id(AGENT_ID); 33 | let is_composing = Rc::new(Cell::new(false)); 34 | { 35 | let style = input.style(); 36 | // Transparent 37 | style.set_property("opacity", "0").unwrap(); 38 | // Hide under canvas 39 | style.set_property("z-index", "-1").unwrap(); 40 | } 41 | // Set size as small as possible, in case user may click on it. 42 | input.set_size(1); 43 | input.set_autofocus(true); 44 | input.set_hidden(true); 45 | 46 | // When IME is off 47 | runner_ref.add_event_listener(&input, "input", { 48 | let input_clone = input.clone(); 49 | let is_composing = is_composing.clone(); 50 | 51 | move |_event: web_sys::InputEvent, runner| { 52 | let text = input_clone.value(); 53 | if !text.is_empty() && !is_composing.get() { 54 | input_clone.set_value(""); 55 | runner.input.raw.events.push(egui::Event::Text(text)); 56 | runner.needs_repaint.repaint_asap(); 57 | } 58 | } 59 | })?; 60 | 61 | { 62 | // When IME is on, handle composition event 63 | runner_ref.add_event_listener(&input, "compositionstart", { 64 | let input_clone = input.clone(); 65 | let is_composing = is_composing.clone(); 66 | 67 | move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| { 68 | is_composing.set(true); 69 | input_clone.set_value(""); 70 | 71 | runner.input.raw.events.push(egui::Event::CompositionStart); 72 | runner.needs_repaint.repaint_asap(); 73 | } 74 | })?; 75 | 76 | runner_ref.add_event_listener( 77 | &input, 78 | "compositionupdate", 79 | move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { 80 | if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { 81 | runner.input.raw.events.push(event); 82 | runner.needs_repaint.repaint_asap(); 83 | } 84 | }, 85 | )?; 86 | 87 | runner_ref.add_event_listener(&input, "compositionend", { 88 | let input_clone = input.clone(); 89 | 90 | move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { 91 | is_composing.set(false); 92 | input_clone.set_value(""); 93 | 94 | if let Some(event) = event.data().map(egui::Event::CompositionEnd) { 95 | runner.input.raw.events.push(event); 96 | runner.needs_repaint.repaint_asap(); 97 | } 98 | } 99 | })?; 100 | } 101 | 102 | // When input lost focus, focus on it again. 103 | // It is useful when user click somewhere outside canvas. 104 | runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| { 105 | // Delay 10 ms, and focus again. 106 | let func = js_sys::Function::new_no_args(&format!( 107 | "document.getElementById('{}').focus()", 108 | AGENT_ID 109 | )); 110 | window 111 | .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) 112 | .unwrap(); 113 | })?; 114 | 115 | body.append_child(&input)?; 116 | 117 | Ok(()) 118 | } 119 | 120 | /// Focus or blur text agent to toggle mobile keyboard. 121 | pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> { 122 | use web_sys::HtmlInputElement; 123 | let window = web_sys::window()?; 124 | let document = window.document()?; 125 | let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap(); 126 | let canvas_style = canvas_element(runner.canvas_id())?.style(); 127 | 128 | if runner.mutable_text_under_cursor { 129 | let is_already_editing = input.hidden(); 130 | if is_already_editing { 131 | input.set_hidden(false); 132 | input.focus().ok()?; 133 | 134 | // Move up canvas so that text edit is shown at ~30% of screen height. 135 | // Only on touch screens, when keyboard popups. 136 | if let Some(latest_touch_pos) = runner.input.latest_touch_pos { 137 | let window_height = window.inner_height().ok()?.as_f64()? as f32; 138 | let current_rel = latest_touch_pos.y / window_height; 139 | 140 | // estimated amount of screen covered by keyboard 141 | let keyboard_fraction = 0.5; 142 | 143 | if current_rel > keyboard_fraction { 144 | // below the keyboard 145 | 146 | let target_rel = 0.3; 147 | 148 | // Note: `delta` is negative, since we are moving the canvas UP 149 | let delta = target_rel - current_rel; 150 | 151 | let delta = delta.max(-keyboard_fraction); // Don't move it crazy much 152 | 153 | let new_pos_percent = format!("{}%", (delta * 100.0).round()); 154 | 155 | canvas_style.set_property("position", "absolute").ok()?; 156 | canvas_style.set_property("top", &new_pos_percent).ok()?; 157 | } 158 | } 159 | } 160 | } else { 161 | // Holding the runner lock while calling input.blur() causes a panic. 162 | // This is most probably caused by the browser running the event handler 163 | // for the triggered blur event synchronously, meaning that the mutex 164 | // lock does not get dropped by the time another event handler is called. 165 | // 166 | // Why this didn't exist before #1290 is a mystery to me, but it exists now 167 | // and this apparently is the fix for it 168 | // 169 | // ¯\_(ツ)_/¯ - @DusterTheFirst 170 | 171 | // So since we are inside a runner lock here, we just postpone the blur/hide: 172 | 173 | call_after_delay(std::time::Duration::from_millis(0), move || { 174 | input.blur().ok(); 175 | input.set_hidden(true); 176 | canvas_style.set_property("position", "absolute").ok(); 177 | canvas_style.set_property("top", "0%").ok(); // move back to normal position 178 | }); 179 | } 180 | Some(()) 181 | } 182 | 183 | fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + 'static) { 184 | use wasm_bindgen::prelude::*; 185 | let window = web_sys::window().unwrap(); 186 | let closure = Closure::once(f); 187 | let delay_ms = delay.as_millis() as _; 188 | window 189 | .set_timeout_with_callback_and_timeout_and_arguments_0( 190 | closure.as_ref().unchecked_ref(), 191 | delay_ms, 192 | ) 193 | .unwrap(); 194 | closure.forget(); // We must forget it, or else the callback is canceled on drop 195 | } 196 | 197 | /// If context is running under mobile device? 198 | fn is_mobile() -> Option { 199 | const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; 200 | 201 | let user_agent = web_sys::window()?.navigator().user_agent().ok()?; 202 | let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name)); 203 | Some(is_mobile) 204 | } 205 | 206 | // Move text agent to text cursor's position, on desktop/laptop, 207 | // candidate window moves following text element (agent), 208 | // so it appears that the IME candidate window moves with text cursor. 209 | // On mobile devices, there is no need to do that. 210 | pub fn move_text_cursor(cursor: Option, canvas_id: &str) -> Option<()> { 211 | let style = text_agent().style(); 212 | // Note: movint agent on mobile devices will lead to unpredictable scroll. 213 | if is_mobile() == Some(false) { 214 | cursor.as_ref().and_then(|&egui::Pos2 { x, y }| { 215 | let canvas = canvas_element(canvas_id)?; 216 | let bounding_rect = text_agent().get_bounding_client_rect(); 217 | let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32) 218 | .min(canvas.client_height() as f32 - bounding_rect.height() as f32); 219 | let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32; 220 | // Canvas is translated 50% horizontally in html. 221 | let x = (x - canvas.offset_width() as f32 / 2.0) 222 | .min(canvas.client_width() as f32 - bounding_rect.width() as f32); 223 | style.set_property("position", "absolute").ok()?; 224 | style.set_property("top", &format!("{}px", y)).ok()?; 225 | style.set_property("left", &format!("{}px", x)).ok() 226 | }) 227 | } else { 228 | style.set_property("position", "absolute").ok()?; 229 | style.set_property("top", "0px").ok()?; 230 | style.set_property("left", "0px").ok() 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /crates/eframe/src/web/web_logger.rs: -------------------------------------------------------------------------------- 1 | /// Implements [`log::Log`] to log messages to `console.log`, `console.warn`, etc. 2 | pub struct WebLogger { 3 | filter: log::LevelFilter, 4 | } 5 | 6 | impl WebLogger { 7 | /// Pipe all [`log`] events to the web console. 8 | pub fn init(filter: log::LevelFilter) -> Result<(), log::SetLoggerError> { 9 | log::set_max_level(filter); 10 | log::set_boxed_logger(Box::new(WebLogger::new(filter))) 11 | } 12 | 13 | pub fn new(filter: log::LevelFilter) -> Self { 14 | Self { filter } 15 | } 16 | } 17 | 18 | impl log::Log for WebLogger { 19 | fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { 20 | metadata.level() <= self.filter 21 | } 22 | 23 | fn log(&self, record: &log::Record<'_>) { 24 | if !self.enabled(record.metadata()) { 25 | return; 26 | } 27 | 28 | let msg = if let (Some(file), Some(line)) = (record.file(), record.line()) { 29 | let file = shorten_file_path(file); 30 | format!("[{}] {file}:{line}: {}", record.target(), record.args()) 31 | } else { 32 | format!("[{}] {}", record.target(), record.args()) 33 | }; 34 | 35 | match record.level() { 36 | log::Level::Trace => console::trace(&msg), 37 | log::Level::Debug => console::debug(&msg), 38 | log::Level::Info => console::info(&msg), 39 | log::Level::Warn => console::warn(&msg), 40 | 41 | // Using console.error causes crashes for unknown reason 42 | // https://github.com/emilk/egui/pull/2961 43 | // log::Level::Error => console::error(&msg), 44 | log::Level::Error => console::warn(&format!("ERROR: {msg}")), 45 | } 46 | } 47 | 48 | fn flush(&self) {} 49 | } 50 | 51 | /// js-bindings for console.log, console.warn, etc 52 | mod console { 53 | use wasm_bindgen::prelude::*; 54 | 55 | #[wasm_bindgen] 56 | extern "C" { 57 | /// `console.trace` 58 | #[wasm_bindgen(js_namespace = console)] 59 | pub fn trace(s: &str); 60 | 61 | /// `console.debug` 62 | #[wasm_bindgen(js_namespace = console)] 63 | pub fn debug(s: &str); 64 | 65 | /// `console.info` 66 | #[wasm_bindgen(js_namespace = console)] 67 | pub fn info(s: &str); 68 | 69 | /// `console.warn` 70 | #[wasm_bindgen(js_namespace = console)] 71 | pub fn warn(s: &str); 72 | 73 | // Using console.error causes crashes for unknown reason 74 | // https://github.com/emilk/egui/pull/2961 75 | // /// `console.error` 76 | // #[wasm_bindgen(js_namespace = console)] 77 | // pub fn error(s: &str); 78 | } 79 | } 80 | 81 | /// Shorten a path to a Rust source file. 82 | /// 83 | /// Example input: 84 | /// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs` 85 | /// * `crates/rerun/src/main.rs` 86 | /// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs` 87 | /// 88 | /// Example output: 89 | /// * `tokio-1.24.1/src/runtime/runtime.rs` 90 | /// * `rerun/src/main.rs` 91 | /// * `core/src/ops/function.rs` 92 | #[allow(dead_code)] // only used on web and in tests 93 | fn shorten_file_path(file_path: &str) -> &str { 94 | if let Some(i) = file_path.rfind("/src/") { 95 | if let Some(prev_slash) = file_path[..i].rfind('/') { 96 | &file_path[prev_slash + 1..] 97 | } else { 98 | file_path 99 | } 100 | } else { 101 | file_path 102 | } 103 | } 104 | 105 | #[test] 106 | fn test_shorten_file_path() { 107 | for (before, after) in [ 108 | ("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"), 109 | ("crates/rerun/src/main.rs", "rerun/src/main.rs"), 110 | ("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"), 111 | ("/weird/path/file.rs", "/weird/path/file.rs"), 112 | ] 113 | { 114 | assert_eq!(shorten_file_path(before), after); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crates/eframe/src/web/web_painter.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | 3 | /// Renderer for a browser canvas. 4 | /// As of writing we're not allowing to decide on the painter at runtime, 5 | /// therefore this trait is merely there for specifying and documenting the interface. 6 | pub(crate) trait WebPainter { 7 | // Create a new web painter targeting a given canvas. 8 | // fn new(canvas_id: &str, options: &WebOptions) -> Result 9 | // where 10 | // Self: Sized; 11 | 12 | /// Id of the canvas in use. 13 | fn canvas_id(&self) -> &str; 14 | 15 | /// Maximum size of a texture in one direction. 16 | fn max_texture_side(&self) -> usize; 17 | 18 | /// Update all internal textures and paint gui. 19 | fn paint_and_update_textures( 20 | &mut self, 21 | clear_color: [f32; 4], 22 | clipped_primitives: &[egui::ClippedPrimitive], 23 | pixels_per_point: f32, 24 | textures_delta: &egui::TexturesDelta, 25 | ) -> Result<(), JsValue>; 26 | 27 | /// Destroy all resources. 28 | fn destroy(&mut self); 29 | } 30 | -------------------------------------------------------------------------------- /crates/eframe/src/web/web_painter_glow.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsCast; 2 | use wasm_bindgen::JsValue; 3 | use web_sys::HtmlCanvasElement; 4 | 5 | use egui_glow::glow; 6 | 7 | use crate::{WebGlContextOption, WebOptions}; 8 | 9 | use super::web_painter::WebPainter; 10 | 11 | pub(crate) struct WebPainterGlow { 12 | canvas: HtmlCanvasElement, 13 | canvas_id: String, 14 | painter: egui_glow::Painter, 15 | } 16 | 17 | impl WebPainterGlow { 18 | pub fn gl(&self) -> &std::sync::Arc { 19 | self.painter.gl() 20 | } 21 | 22 | pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { 23 | let canvas = super::canvas_element_or_die(canvas_id); 24 | 25 | let (gl, shader_prefix) = 26 | init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; 27 | let gl = std::sync::Arc::new(gl); 28 | 29 | let painter = egui_glow::Painter::new(gl, shader_prefix, None) 30 | .map_err(|error| format!("Error starting glow painter: {}", error))?; 31 | 32 | Ok(Self { 33 | canvas, 34 | canvas_id: canvas_id.to_owned(), 35 | painter, 36 | }) 37 | } 38 | } 39 | 40 | impl WebPainter for WebPainterGlow { 41 | fn max_texture_side(&self) -> usize { 42 | self.painter.max_texture_side() 43 | } 44 | 45 | fn canvas_id(&self) -> &str { 46 | &self.canvas_id 47 | } 48 | 49 | fn paint_and_update_textures( 50 | &mut self, 51 | clear_color: [f32; 4], 52 | clipped_primitives: &[egui::ClippedPrimitive], 53 | pixels_per_point: f32, 54 | textures_delta: &egui::TexturesDelta, 55 | ) -> Result<(), JsValue> { 56 | let canvas_dimension = [self.canvas.width(), self.canvas.height()]; 57 | 58 | for (id, image_delta) in &textures_delta.set { 59 | self.painter.set_texture(*id, image_delta); 60 | } 61 | 62 | egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color); 63 | self.painter 64 | .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); 65 | 66 | for &id in &textures_delta.free { 67 | self.painter.free_texture(id); 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn destroy(&mut self) { 74 | self.painter.destroy(); 75 | } 76 | } 77 | 78 | /// Returns glow context and shader prefix. 79 | fn init_glow_context_from_canvas( 80 | canvas: &HtmlCanvasElement, 81 | options: WebGlContextOption, 82 | ) -> Result<(glow::Context, &'static str), String> { 83 | let result = match options { 84 | // Force use WebGl1 85 | WebGlContextOption::WebGl1 => init_webgl1(canvas), 86 | // Force use WebGl2 87 | WebGlContextOption::WebGl2 => init_webgl2(canvas), 88 | // Trying WebGl2 first 89 | WebGlContextOption::BestFirst => init_webgl2(canvas).or_else(|| init_webgl1(canvas)), 90 | // Trying WebGl1 first (useful for testing). 91 | WebGlContextOption::CompatibilityFirst => { 92 | init_webgl1(canvas).or_else(|| init_webgl2(canvas)) 93 | } 94 | }; 95 | 96 | if let Some(result) = result { 97 | Ok(result) 98 | } else { 99 | Err("WebGL isn't supported".into()) 100 | } 101 | } 102 | 103 | fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> { 104 | let gl1_ctx = canvas 105 | .get_context("webgl") 106 | .expect("Failed to query about WebGL2 context"); 107 | 108 | let gl1_ctx = gl1_ctx?; 109 | log::debug!("WebGL1 selected."); 110 | 111 | let gl1_ctx = gl1_ctx 112 | .dyn_into::() 113 | .unwrap(); 114 | 115 | let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) { 116 | log::debug!("Enabling webkitGTK brightening workaround."); 117 | "#define APPLY_BRIGHTENING_GAMMA" 118 | } else { 119 | "" 120 | }; 121 | 122 | let gl = glow::Context::from_webgl1_context(gl1_ctx); 123 | 124 | Some((gl, shader_prefix)) 125 | } 126 | 127 | fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> { 128 | let gl2_ctx = canvas 129 | .get_context("webgl2") 130 | .expect("Failed to query about WebGL2 context"); 131 | 132 | let gl2_ctx = gl2_ctx?; 133 | log::debug!("WebGL2 selected."); 134 | 135 | let gl2_ctx = gl2_ctx 136 | .dyn_into::() 137 | .unwrap(); 138 | let gl = glow::Context::from_webgl2_context(gl2_ctx); 139 | let shader_prefix = ""; 140 | 141 | Some((gl, shader_prefix)) 142 | } 143 | 144 | fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool { 145 | // See https://github.com/emilk/egui/issues/794 146 | 147 | // detect WebKitGTK 148 | 149 | // WebKitGTK use WebKit default unmasked vendor and renderer 150 | // but safari use same vendor and renderer 151 | // so exclude "Mac OS X" user-agent. 152 | let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap(); 153 | !user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl) 154 | } 155 | 156 | /// detecting Safari and `webkitGTK`. 157 | /// 158 | /// Safari and `webkitGTK` use unmasked renderer :Apple GPU 159 | /// 160 | /// If we detect safari or `webkitGTKs` returns true. 161 | /// 162 | /// This function used to avoid displaying linear color with `sRGB` supported systems. 163 | fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool { 164 | // This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.") 165 | // but unless we call it we get errors in Chrome when we call `get_parameter` below. 166 | // TODO(emilk): do something smart based on user agent? 167 | if gl 168 | .get_extension("WEBGL_debug_renderer_info") 169 | .unwrap() 170 | .is_some() 171 | { 172 | if let Ok(renderer) = 173 | gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL) 174 | { 175 | if let Some(renderer) = renderer.as_string() { 176 | if renderer.contains("Apple") { 177 | return true; 178 | } 179 | } 180 | } 181 | } 182 | 183 | false 184 | } 185 | -------------------------------------------------------------------------------- /crates/eframe/src/web/web_runner.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | use crate::{epi, App}; 6 | 7 | use super::{events, AppRunner, PanicHandler}; 8 | 9 | /// This is how `eframe` runs your wepp application 10 | /// 11 | /// This is cheap to clone. 12 | /// 13 | /// See [the crate level docs](crate) for an example. 14 | #[derive(Clone)] 15 | pub struct WebRunner { 16 | /// Have we ever panicked? 17 | panic_handler: PanicHandler, 18 | 19 | /// If we ever panic during running, this RefCell is poisoned. 20 | /// So before we use it, we need to check [`Self::panic_handler`]. 21 | runner: Rc>>, 22 | 23 | /// In case of a panic, unsubscribe these. 24 | /// They have to be in a separate `Rc` so that we don't need to pass them to 25 | /// the panic handler, since they aren't `Send`. 26 | events_to_unsubscribe: Rc>>, 27 | } 28 | 29 | impl WebRunner { 30 | // Will install a panic handler that will catch and log any panics 31 | #[allow(clippy::new_without_default)] 32 | pub fn new() -> Self { 33 | #[cfg(not(web_sys_unstable_apis))] 34 | log::warn!( 35 | "eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work." 36 | ); 37 | 38 | let panic_handler = PanicHandler::install(); 39 | 40 | Self { 41 | panic_handler, 42 | runner: Rc::new(RefCell::new(None)), 43 | events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), 44 | } 45 | } 46 | 47 | /// Create the application, install callbacks, and start running the app. 48 | /// 49 | /// # Errors 50 | /// Failing to initialize graphics. 51 | pub async fn start( 52 | &self, 53 | canvas_id: &str, 54 | web_options: crate::WebOptions, 55 | app_creator: epi::AppCreator, 56 | ) -> Result<(), JsValue> { 57 | self.destroy(); 58 | 59 | let follow_system_theme = web_options.follow_system_theme; 60 | 61 | let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; 62 | runner.warm_up(); 63 | self.runner.replace(Some(runner)); 64 | 65 | { 66 | events::install_canvas_events(self)?; 67 | events::install_document_events(self)?; 68 | events::install_window_events(self)?; 69 | super::text_agent::install_text_agent(self)?; 70 | 71 | if follow_system_theme { 72 | events::install_color_scheme_change_event(self)?; 73 | } 74 | 75 | events::request_animation_frame(self.clone())?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | 81 | /// Has there been a panic? 82 | pub fn has_panicked(&self) -> bool { 83 | self.panic_handler.has_panicked() 84 | } 85 | 86 | /// What was the panic message and callstack? 87 | pub fn panic_summary(&self) -> Option { 88 | self.panic_handler.panic_summary() 89 | } 90 | 91 | fn unsubscribe_from_all_events(&self) { 92 | let events_to_unsubscribe: Vec<_> = 93 | std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut()); 94 | 95 | if !events_to_unsubscribe.is_empty() { 96 | log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len()); 97 | for x in events_to_unsubscribe { 98 | if let Err(err) = x.unsubscribe() { 99 | log::warn!("Failed to unsubscribe from event: {err:?}"); 100 | } 101 | } 102 | } 103 | } 104 | 105 | pub fn destroy(&self) { 106 | self.unsubscribe_from_all_events(); 107 | 108 | if let Some(runner) = self.runner.replace(None) { 109 | runner.destroy(); 110 | } 111 | } 112 | 113 | /// Returns `None` if there has been a panic, or if we have been destroyed. 114 | /// In that case, just return to JS. 115 | pub(crate) fn try_lock(&self) -> Option> { 116 | if self.panic_handler.has_panicked() { 117 | // Unsubscribe from all events so that we don't get any more callbacks 118 | // that will try to access the poisoned runner. 119 | self.unsubscribe_from_all_events(); 120 | None 121 | } else { 122 | let lock = self.runner.try_borrow_mut().ok()?; 123 | std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() }) 124 | .ok() 125 | } 126 | } 127 | 128 | /// Get mutable access to the concrete [`App`] we enclose. 129 | /// 130 | /// This will panic if your app does not implement [`App::as_any_mut`], 131 | /// and return `None` if this runner has panicked. 132 | pub fn app_mut( 133 | &self, 134 | ) -> Option> { 135 | self.try_lock() 136 | .map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::())) 137 | } 138 | 139 | /// Convenience function to reduce boilerplate and ensure that all event handlers 140 | /// are dealt with in the same way. 141 | /// 142 | /// All events added with this method will automatically be unsubscribed on panic, 143 | /// or when [`Self::destroy`] is called. 144 | pub fn add_event_listener( 145 | &self, 146 | target: &web_sys::EventTarget, 147 | event_name: &'static str, 148 | mut closure: impl FnMut(E, &mut AppRunner) + 'static, 149 | ) -> Result<(), wasm_bindgen::JsValue> { 150 | let runner_ref = self.clone(); 151 | 152 | // Create a JS closure based on the FnMut provided 153 | let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { 154 | // Only call the wrapped closure if the egui code has not panicked 155 | if let Some(mut runner_lock) = runner_ref.try_lock() { 156 | // Cast the event to the expected event type 157 | let event = event.unchecked_into::(); 158 | closure(event, &mut runner_lock); 159 | } 160 | }) as Box); 161 | 162 | // Add the event listener to the target 163 | target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; 164 | 165 | let handle = TargetEvent { 166 | target: target.clone(), 167 | event_name: event_name.to_owned(), 168 | closure, 169 | }; 170 | 171 | // Remember it so we unsubscribe on panic. 172 | // Otherwise we get calls into `self.runner` after it has been poisoned by a panic. 173 | self.events_to_unsubscribe 174 | .borrow_mut() 175 | .push(EventToUnsubscribe::TargetEvent(handle)); 176 | 177 | Ok(()) 178 | } 179 | } 180 | 181 | // ---------------------------------------------------------------------------- 182 | 183 | struct TargetEvent { 184 | target: web_sys::EventTarget, 185 | event_name: String, 186 | closure: Closure, 187 | } 188 | 189 | #[allow(unused)] 190 | struct IntervalHandle { 191 | handle: i32, 192 | closure: Closure, 193 | } 194 | 195 | enum EventToUnsubscribe { 196 | TargetEvent(TargetEvent), 197 | 198 | #[allow(unused)] 199 | IntervalHandle(IntervalHandle), 200 | } 201 | 202 | impl EventToUnsubscribe { 203 | pub fn unsubscribe(self) -> Result<(), JsValue> { 204 | match self { 205 | EventToUnsubscribe::TargetEvent(handle) => { 206 | handle.target.remove_event_listener_with_callback( 207 | handle.event_name.as_str(), 208 | handle.closure.as_ref().unchecked_ref(), 209 | )?; 210 | Ok(()) 211 | } 212 | EventToUnsubscribe::IntervalHandle(handle) => { 213 | let window = web_sys::window().unwrap(); 214 | window.clear_interval_with_handle(handle.handle); 215 | Ok(()) 216 | } 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /crates/egui-winit/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for egui-winit 2 | 3 | ## \[0.23.0] 4 | 5 | - [`b41f29b4`](https://github.com/tauri-apps/egui/commit/b41f29b4dd80dfef2ed65ce8acc98977fc6f63aa) Upgraded to use egui 0.22.0 6 | 7 | All notable changes to the `egui-winit` integration will be noted in this file. 8 | 9 | ## Unreleased 10 | 11 | ## 0.22.0 - 2023-05-23 12 | 13 | - Only use `wasm-bindgen` feature for `instant` when building for wasm32 [#2808](https://github.com/emilk/egui/pull/2808) (thanks [@gferon](https://github.com/gferon)!) 14 | - Fix unsafe API of `Clipboard::new` [#2765](https://github.com/emilk/egui/pull/2765) (thanks [@dhardy](https://github.com/dhardy)!) 15 | - Remove `android-activity` dependency + add `Activity` backend features [#2863](https://github.com/emilk/egui/pull/2863) (thanks [@rib](https://github.com/rib)!) 16 | - Use `RawDisplayHandle` for smithay clipboard init [#2914](https://github.com/emilk/egui/pull/2914) (thanks [@lunixbochs](https://github.com/lunixbochs)!) 17 | - Clear all keys and modifies on focus change [#2933](https://github.com/emilk/egui/pull/2933) 18 | - Support Wasm target [#2949](https://github.com/emilk/egui/pull/2949) (thanks [@jinleili](https://github.com/jinleili)!) 19 | - Fix unsafe API: remove `State::new_with_wayland_display`; change `Clipboard::new` to take `&EventLoopWindowTarget` 20 | 21 | ## 0.21.1 - 2023-02-12 22 | 23 | - Fixed crash when window position is in an invalid state, which could happen e.g. due to changes in monitor size or DPI ([#2722](https://github.com/emilk/egui/issues/2722)). 24 | 25 | ## 0.21.0 - 2023-02-08 26 | 27 | - Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)). 28 | - Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)). 29 | - Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)). 30 | - Fix bug where the cursor could get stuck using the wrong icon. 31 | 32 | ## 0.20.1 - 2022-12-11 33 | 34 | - Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)). 35 | 36 | ## 0.20.0 - 2022-12-08 37 | 38 | - The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)). 39 | - Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)). 40 | - Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)). 41 | - Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)). 42 | 43 | ## 0.19.0 - 2022-08-20 44 | 45 | - MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). 46 | - Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)). 47 | - Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)). 48 | - Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)). 49 | - Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)). 50 | - Use the new `RawInput::has_focus` field to indicate whether the window has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)). 51 | 52 | ## 0.18.0 - 2022-04-30 53 | 54 | - Reexport `egui` crate 55 | - MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)). 56 | - Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)). 57 | - Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)). 58 | - Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)). 59 | - Removed the features `dark-light` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)). 60 | 61 | ## 0.17.0 - 2022-02-22 62 | 63 | - Fixed horizontal scrolling direction on Linux. 64 | - Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatibility ([#1023](https://github.com/emilk/egui/pull/1023)) 65 | - Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)). 66 | - Fixed `enable_drag` on Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)). 67 | - Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). 68 | - Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)). 69 | 70 | ## 0.16.0 - 2021-12-29 71 | 72 | - Added helper `EpiIntegration` ([#871](https://github.com/emilk/egui/pull/871)). 73 | - Fixed shift key getting stuck enabled with the X11 option `shift:both_capslock` enabled ([#849](https://github.com/emilk/egui/pull/849)). 74 | - Removed `State::is_quit_event` and `State::is_quit_shortcut` ([#881](https://github.com/emilk/egui/pull/881)). 75 | - Updated `winit` to 0.26 ([#930](https://github.com/emilk/egui/pull/930)). 76 | 77 | ## 0.15.0 - 2021-10-24 78 | 79 | First stand-alone release. Previously part of `egui_glium`. 80 | -------------------------------------------------------------------------------- /crates/egui-winit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-tao" 3 | version = "0.23.0" 4 | authors = [ "Emil Ernerfeldt " ] 5 | description = "Bindings for using egui with winit" 6 | edition = "2021" 7 | rust-version = "1.65" 8 | homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit" 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit" 12 | categories = [ "gui", "game-development" ] 13 | keywords = [ "winit", "egui", "gui", "gamedev" ] 14 | include = [ 15 | "../LICENSE-APACHE", 16 | "../LICENSE-MIT", 17 | "**/*.rs", 18 | "Cargo.toml" 19 | ] 20 | 21 | [package.metadata.docs.rs] 22 | all-features = true 23 | 24 | [features] 25 | default = [ "clipboard", "links" ] 26 | bytemuck = [ "egui/bytemuck" ] 27 | clipboard = [ "arboard", "smithay-clipboard" ] 28 | links = [ "webbrowser" ] 29 | puffin = [ "dep:puffin" ] 30 | serde = [ "egui/serde", "dep:serde" ] 31 | 32 | [dependencies] 33 | egui = { version = "0.22.0", default-features = false, features = [ "log" ] } 34 | log = { version = "0.4", features = [ "std" ] } 35 | winit = { package = "tao", version = "0.19.0", default-features = false } 36 | raw-window-handle = "0.5.0" 37 | document-features = { version = "0.2", optional = true } 38 | puffin = { version = "0.15", optional = true } 39 | serde = { version = "1.0", optional = true, features = [ "derive" ] } 40 | webbrowser = { version = "0.8.3", optional = true } 41 | 42 | [target."cfg(not(target_arch=\"wasm32\"))".dependencies] 43 | instant = { version = "0.1" } 44 | 45 | [target."cfg(target_arch=\"wasm32\")".dependencies] 46 | instant = { version = "0.1", features = [ "wasm-bindgen" ] } 47 | 48 | [target."cfg(any(target_os=\"linux\", target_os=\"dragonfly\", target_os=\"freebsd\", target_os=\"netbsd\", target_os=\"openbsd\"))".dependencies] 49 | smithay-clipboard = { version = "0.6.3", optional = true } 50 | 51 | [target."cfg(not(target_os = \"android\"))".dependencies] 52 | arboard = { version = "3.2", optional = true, default-features = false } 53 | -------------------------------------------------------------------------------- /crates/egui-winit/README.md: -------------------------------------------------------------------------------- 1 | # egui-winit 2 | 3 | [![Latest version](https://img.shields.io/crates/v/egui-tao.svg)](https://crates.io/crates/egui-tao) 4 | [![Documentation](https://docs.rs/egui-tao/badge.svg)](https://docs.rs/egui-tao) 5 | ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 6 | ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) 7 | 8 | This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [`tao`](https://crates.io/crates/tao). 9 | 10 | The library translates tao events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc. 11 | -------------------------------------------------------------------------------- /crates/egui-winit/src/clipboard.rs: -------------------------------------------------------------------------------- 1 | use raw_window_handle::HasRawDisplayHandle; 2 | 3 | /// Handles interfacing with the OS clipboard. 4 | /// 5 | /// If the "clipboard" feature is off, or we cannot connect to the OS clipboard, 6 | /// then a fallback clipboard that just works works within the same app is used instead. 7 | pub struct Clipboard { 8 | #[cfg(all(feature = "arboard", not(target_os = "android")))] 9 | arboard: Option, 10 | 11 | #[cfg(all( 12 | any( 13 | target_os = "linux", 14 | target_os = "dragonfly", 15 | target_os = "freebsd", 16 | target_os = "netbsd", 17 | target_os = "openbsd" 18 | ), 19 | feature = "smithay-clipboard" 20 | ))] 21 | smithay: Option, 22 | 23 | /// Fallback manual clipboard. 24 | clipboard: String, 25 | } 26 | 27 | impl Clipboard { 28 | /// Construct a new instance 29 | /// 30 | /// # Safety 31 | /// 32 | /// The returned `Clipboard` must not outlive the input `_display_target`. 33 | pub fn new(_display_target: &dyn HasRawDisplayHandle) -> Self { 34 | Self { 35 | #[cfg(all(feature = "arboard", not(target_os = "android")))] 36 | arboard: init_arboard(), 37 | 38 | #[cfg(all( 39 | any( 40 | target_os = "linux", 41 | target_os = "dragonfly", 42 | target_os = "freebsd", 43 | target_os = "netbsd", 44 | target_os = "openbsd" 45 | ), 46 | feature = "smithay-clipboard" 47 | ))] 48 | smithay: init_smithay_clipboard(_display_target), 49 | 50 | clipboard: Default::default(), 51 | } 52 | } 53 | 54 | pub fn get(&mut self) -> Option { 55 | #[cfg(all( 56 | any( 57 | target_os = "linux", 58 | target_os = "dragonfly", 59 | target_os = "freebsd", 60 | target_os = "netbsd", 61 | target_os = "openbsd" 62 | ), 63 | feature = "smithay-clipboard" 64 | ))] 65 | if let Some(clipboard) = &mut self.smithay { 66 | return match clipboard.load() { 67 | Ok(text) => Some(text), 68 | Err(err) => { 69 | log::error!("smithay paste error: {err}"); 70 | None 71 | } 72 | }; 73 | } 74 | 75 | #[cfg(all(feature = "arboard", not(target_os = "android")))] 76 | if let Some(clipboard) = &mut self.arboard { 77 | return match clipboard.get_text() { 78 | Ok(text) => Some(text), 79 | Err(err) => { 80 | log::error!("arboard paste error: {err}"); 81 | None 82 | } 83 | }; 84 | } 85 | 86 | Some(self.clipboard.clone()) 87 | } 88 | 89 | pub fn set(&mut self, text: String) { 90 | #[cfg(all( 91 | any( 92 | target_os = "linux", 93 | target_os = "dragonfly", 94 | target_os = "freebsd", 95 | target_os = "netbsd", 96 | target_os = "openbsd" 97 | ), 98 | feature = "smithay-clipboard" 99 | ))] 100 | if let Some(clipboard) = &mut self.smithay { 101 | clipboard.store(text); 102 | return; 103 | } 104 | 105 | #[cfg(all(feature = "arboard", not(target_os = "android")))] 106 | if let Some(clipboard) = &mut self.arboard { 107 | if let Err(err) = clipboard.set_text(text) { 108 | log::error!("arboard copy/cut error: {err}"); 109 | } 110 | return; 111 | } 112 | 113 | self.clipboard = text; 114 | } 115 | } 116 | 117 | #[cfg(all(feature = "arboard", not(target_os = "android")))] 118 | fn init_arboard() -> Option { 119 | log::debug!("Initializing arboard clipboard…"); 120 | match arboard::Clipboard::new() { 121 | Ok(clipboard) => Some(clipboard), 122 | Err(err) => { 123 | log::warn!("Failed to initialize arboard clipboard: {err}"); 124 | None 125 | } 126 | } 127 | } 128 | 129 | #[cfg(all( 130 | any( 131 | target_os = "linux", 132 | target_os = "dragonfly", 133 | target_os = "freebsd", 134 | target_os = "netbsd", 135 | target_os = "openbsd" 136 | ), 137 | feature = "smithay-clipboard" 138 | ))] 139 | fn init_smithay_clipboard( 140 | _display_target: &dyn HasRawDisplayHandle, 141 | ) -> Option { 142 | use raw_window_handle::RawDisplayHandle; 143 | if let RawDisplayHandle::Wayland(display) = _display_target.raw_display_handle() { 144 | log::debug!("Initializing smithay clipboard…"); 145 | #[allow(unsafe_code)] 146 | Some(unsafe { smithay_clipboard::Clipboard::new(display.display) }) 147 | } else { 148 | #[cfg(feature = "wayland")] 149 | log::debug!("Cannot init smithay clipboard without a Wayland display handle"); 150 | #[cfg(not(feature = "wayland"))] 151 | log::debug!( 152 | "Cannot init smithay clipboard: the 'wayland' feature of 'egui-winit' is not enabled" 153 | ); 154 | None 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crates/egui-winit/src/window_settings.rs: -------------------------------------------------------------------------------- 1 | /// Can be used to store native window settings (position and size). 2 | #[derive(Clone, Copy, Debug)] 3 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 4 | pub struct WindowSettings { 5 | /// Position of window in physical pixels. This is either 6 | /// the inner or outer position depending on the platform. 7 | /// See [`winit::window::WindowBuilder::with_position`] for details. 8 | position: Option, 9 | 10 | fullscreen: bool, 11 | 12 | /// Inner size of window in logical pixels 13 | inner_size_points: Option, 14 | } 15 | 16 | impl WindowSettings { 17 | pub fn from_display(window: &winit::window::Window) -> Self { 18 | let inner_size_points = window.inner_size().to_logical::(window.scale_factor()); 19 | let position = if cfg!(macos) { 20 | // MacOS uses inner position when positioning windows. 21 | window 22 | .inner_position() 23 | .ok() 24 | .map(|p| egui::pos2(p.x as f32, p.y as f32)) 25 | } else { 26 | // Other platforms use the outer position. 27 | window 28 | .outer_position() 29 | .ok() 30 | .map(|p| egui::pos2(p.x as f32, p.y as f32)) 31 | }; 32 | 33 | Self { 34 | position, 35 | 36 | fullscreen: window.fullscreen().is_some(), 37 | 38 | inner_size_points: Some(egui::vec2( 39 | inner_size_points.width, 40 | inner_size_points.height, 41 | )), 42 | } 43 | } 44 | 45 | pub fn inner_size_points(&self) -> Option { 46 | self.inner_size_points 47 | } 48 | 49 | pub fn initialize_window( 50 | &self, 51 | mut window: winit::window::WindowBuilder, 52 | ) -> winit::window::WindowBuilder { 53 | // If the app last ran on two monitors and only one is now connected, then 54 | // the given position is invalid. 55 | // If this happens on Mac, the window is clamped into valid area. 56 | // If this happens on Windows, the clamping behavior is managed by the function 57 | // clamp_window_to_sane_position. 58 | if let Some(pos) = self.position { 59 | window = window.with_position(winit::dpi::PhysicalPosition { 60 | x: pos.x as f64, 61 | y: pos.y as f64, 62 | }); 63 | } 64 | 65 | if let Some(inner_size_points) = self.inner_size_points { 66 | window 67 | .with_inner_size(winit::dpi::LogicalSize { 68 | width: inner_size_points.x as f64, 69 | height: inner_size_points.y as f64, 70 | }) 71 | .with_fullscreen( 72 | self.fullscreen 73 | .then_some(winit::window::Fullscreen::Borderless(None)), 74 | ) 75 | } else { 76 | window 77 | } 78 | } 79 | 80 | pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) { 81 | use egui::NumExt as _; 82 | 83 | if let Some(size) = &mut self.inner_size_points { 84 | // Prevent ridiculously small windows 85 | let min_size = egui::Vec2::splat(64.0); 86 | *size = size.at_least(min_size); 87 | *size = size.at_most(max_size); 88 | } 89 | } 90 | 91 | pub fn clamp_window_to_sane_position( 92 | &mut self, 93 | event_loop: &winit::event_loop::EventLoopWindowTarget, 94 | ) { 95 | if let (Some(position), Some(inner_size_points)) = 96 | (&mut self.position, &self.inner_size_points) 97 | { 98 | let monitors = event_loop.available_monitors(); 99 | // default to primary monitor, in case the correct monitor was disconnected. 100 | let mut active_monitor = if let Some(active_monitor) = event_loop 101 | .primary_monitor() 102 | .or_else(|| event_loop.available_monitors().next()) 103 | { 104 | active_monitor 105 | } else { 106 | return; // no monitors 🤷 107 | }; 108 | for monitor in monitors { 109 | let monitor_x_range = (monitor.position().x - inner_size_points.x as i32) 110 | ..(monitor.position().x + monitor.size().width as i32); 111 | let monitor_y_range = (monitor.position().y - inner_size_points.y as i32) 112 | ..(monitor.position().y + monitor.size().height as i32); 113 | 114 | if monitor_x_range.contains(&(position.x as i32)) 115 | && monitor_y_range.contains(&(position.y as i32)) 116 | { 117 | active_monitor = monitor; 118 | } 119 | } 120 | 121 | let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32); 122 | // Add size of title bar. This is 32 px by default in Win 10/11. 123 | if cfg!(target_os = "windows") { 124 | inner_size_pixels += 125 | egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32); 126 | } 127 | let monitor_position = egui::Pos2::new( 128 | active_monitor.position().x as f32, 129 | active_monitor.position().y as f32, 130 | ); 131 | let monitor_size = egui::Vec2::new( 132 | active_monitor.size().width as f32, 133 | active_monitor.size().height as f32, 134 | ); 135 | 136 | // Window size cannot be negative or the subsequent `clamp` will panic. 137 | let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO); 138 | // To get the maximum position, we get the rightmost corner of the display, then 139 | // subtract the size of the window to get the bottom right most value window.position 140 | // can have. 141 | *position = position.clamp(monitor_position, monitor_position + window_size); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/egui_demo_app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_demo_app" 3 | version = "0.22.0" 4 | authors = ["Emil Ernerfeldt "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | rust-version = "1.65" 8 | publish = false 9 | default-run = "egui_demo_app" 10 | 11 | [package.metadata.docs.rs] 12 | all-features = true 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | 17 | 18 | [features] 19 | default = ["glow", "persistence"] 20 | 21 | http = ["ehttp", "image", "poll-promise", "egui_extras/image"] 22 | persistence = ["eframe/persistence", "egui/persistence", "serde"] 23 | web_screen_reader = ["eframe/web_screen_reader"] # experimental 24 | serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] 25 | syntax_highlighting = ["egui_demo_lib/syntax_highlighting"] 26 | 27 | glow = ["eframe/glow"] 28 | ## wgpu = ["eframe/wgpu", "bytemuck"] 29 | 30 | 31 | [dependencies] 32 | chrono = { version = "0.4", default-features = false, features = [ 33 | "js-sys", 34 | "wasmbind", 35 | ] } 36 | eframe = { package = "eframe_tao", version = "0.23.0", path = "../eframe", default-features = false } 37 | egui = { version = "0.22.0", features = [ 38 | "extra_debug_asserts", 39 | ] } 40 | egui_demo_lib = { version = "0.22.0", features = [ 41 | "chrono", 42 | ] } 43 | log = { version = "0.4", features = ["std"] } 44 | 45 | # Optional dependencies: 46 | 47 | bytemuck = { version = "1.7.1", optional = true } 48 | egui_extras = { version = "0.22.0", optional = true} 49 | 50 | # feature "http": 51 | ehttp = { version = "0.2.0", optional = true } 52 | image = { version = "0.24", optional = true, default-features = false, features = [ 53 | "jpeg", 54 | "png", 55 | ] } 56 | poll-promise = { version = "0.2", optional = true, default-features = false } 57 | 58 | # feature "persistence": 59 | serde = { version = "1", optional = true, features = ["derive"] } 60 | 61 | 62 | # native: 63 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 64 | env_logger = "0.10" 65 | 66 | # web: 67 | [target.'cfg(target_arch = "wasm32")'.dependencies] 68 | wasm-bindgen = "=0.2.86" 69 | wasm-bindgen-futures = "0.4" 70 | web-sys = "0.3" 71 | -------------------------------------------------------------------------------- /crates/egui_demo_app/README.md: -------------------------------------------------------------------------------- 1 | # egui demo app 2 | 3 | This app demonstrates [`egui`](https://github.com/emilk/egui/) and [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). 4 | 5 | The demo app is slightly modified to use `eframe_tao` instead. 6 | 7 | View the demo app online at . 8 | 9 | Run it locally with `cargo run --release -p egui_demo_app`. 10 | 11 | `egui_demo_app` can be compiled to WASM and viewed in a browser locally with: 12 | 13 | ```sh 14 | ./scripts/start_server.sh & 15 | ./scripts/build_demo_web.sh --open 16 | ``` 17 | 18 | `egui_demo_app` uses [`egui_demo_lib`](https://github.com/emilk/egui/tree/master/crates/egui_demo_lib). 19 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/apps/custom3d_glow.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use eframe::egui_glow; 4 | use egui::mutex::Mutex; 5 | use egui_glow::glow; 6 | 7 | pub struct Custom3d { 8 | /// Behind an `Arc>` so we can pass it to [`egui::PaintCallback`] and paint later. 9 | rotating_triangle: Arc>, 10 | angle: f32, 11 | } 12 | 13 | impl Custom3d { 14 | pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option { 15 | let gl = cc.gl.as_ref()?; 16 | Some(Self { 17 | rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl)?)), 18 | angle: 0.0, 19 | }) 20 | } 21 | } 22 | 23 | impl eframe::App for Custom3d { 24 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 25 | egui::CentralPanel::default().show(ctx, |ui| { 26 | egui::ScrollArea::both() 27 | .auto_shrink([false; 2]) 28 | .show(ui, |ui| { 29 | ui.horizontal(|ui| { 30 | ui.spacing_mut().item_spacing.x = 0.0; 31 | ui.label("The triangle is being painted using "); 32 | ui.hyperlink_to("glow", "https://github.com/grovesNL/glow"); 33 | ui.label(" (OpenGL)."); 34 | }); 35 | ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui."); 36 | 37 | egui::Frame::canvas(ui.style()).show(ui, |ui| { 38 | self.custom_painting(ui); 39 | }); 40 | ui.label("Drag to rotate!"); 41 | ui.add(egui_demo_lib::egui_github_link_file!()); 42 | }); 43 | }); 44 | } 45 | 46 | fn on_exit(&mut self, gl: Option<&glow::Context>) { 47 | if let Some(gl) = gl { 48 | self.rotating_triangle.lock().destroy(gl); 49 | } 50 | } 51 | } 52 | 53 | impl Custom3d { 54 | fn custom_painting(&mut self, ui: &mut egui::Ui) { 55 | let (rect, response) = 56 | ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); 57 | 58 | self.angle += response.drag_delta().x * 0.01; 59 | 60 | // Clone locals so we can move them into the paint callback: 61 | let angle = self.angle; 62 | let rotating_triangle = self.rotating_triangle.clone(); 63 | 64 | let cb = egui_glow::CallbackFn::new(move |_info, painter| { 65 | rotating_triangle.lock().paint(painter.gl(), angle); 66 | }); 67 | 68 | let callback = egui::PaintCallback { 69 | rect, 70 | callback: Arc::new(cb), 71 | }; 72 | ui.painter().add(callback); 73 | } 74 | } 75 | 76 | struct RotatingTriangle { 77 | program: glow::Program, 78 | vertex_array: glow::VertexArray, 79 | } 80 | 81 | #[allow(unsafe_code)] // we need unsafe code to use glow 82 | impl RotatingTriangle { 83 | fn new(gl: &glow::Context) -> Option { 84 | use glow::HasContext as _; 85 | 86 | let shader_version = egui_glow::ShaderVersion::get(gl); 87 | 88 | unsafe { 89 | let program = gl.create_program().expect("Cannot create program"); 90 | 91 | if !shader_version.is_new_shader_interface() { 92 | log::warn!( 93 | "Custom 3D painting hasn't been ported to {:?}", 94 | shader_version 95 | ); 96 | return None; 97 | } 98 | 99 | let (vertex_shader_source, fragment_shader_source) = ( 100 | r#" 101 | const vec2 verts[3] = vec2[3]( 102 | vec2(0.0, 1.0), 103 | vec2(-1.0, -1.0), 104 | vec2(1.0, -1.0) 105 | ); 106 | const vec4 colors[3] = vec4[3]( 107 | vec4(1.0, 0.0, 0.0, 1.0), 108 | vec4(0.0, 1.0, 0.0, 1.0), 109 | vec4(0.0, 0.0, 1.0, 1.0) 110 | ); 111 | out vec4 v_color; 112 | uniform float u_angle; 113 | void main() { 114 | v_color = colors[gl_VertexID]; 115 | gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); 116 | gl_Position.x *= cos(u_angle); 117 | } 118 | "#, 119 | r#" 120 | precision mediump float; 121 | in vec4 v_color; 122 | out vec4 out_color; 123 | void main() { 124 | out_color = v_color; 125 | } 126 | "#, 127 | ); 128 | 129 | let shader_sources = [ 130 | (glow::VERTEX_SHADER, vertex_shader_source), 131 | (glow::FRAGMENT_SHADER, fragment_shader_source), 132 | ]; 133 | 134 | let shaders: Vec<_> = shader_sources 135 | .iter() 136 | .map(|(shader_type, shader_source)| { 137 | let shader = gl 138 | .create_shader(*shader_type) 139 | .expect("Cannot create shader"); 140 | gl.shader_source( 141 | shader, 142 | &format!( 143 | "{}\n{}", 144 | shader_version.version_declaration(), 145 | shader_source 146 | ), 147 | ); 148 | gl.compile_shader(shader); 149 | assert!( 150 | gl.get_shader_compile_status(shader), 151 | "Failed to compile custom_3d_glow {shader_type}: {}", 152 | gl.get_shader_info_log(shader) 153 | ); 154 | 155 | gl.attach_shader(program, shader); 156 | shader 157 | }) 158 | .collect(); 159 | 160 | gl.link_program(program); 161 | assert!( 162 | gl.get_program_link_status(program), 163 | "{}", 164 | gl.get_program_info_log(program) 165 | ); 166 | 167 | for shader in shaders { 168 | gl.detach_shader(program, shader); 169 | gl.delete_shader(shader); 170 | } 171 | 172 | let vertex_array = gl 173 | .create_vertex_array() 174 | .expect("Cannot create vertex array"); 175 | 176 | Some(Self { 177 | program, 178 | vertex_array, 179 | }) 180 | } 181 | } 182 | 183 | fn destroy(&self, gl: &glow::Context) { 184 | use glow::HasContext as _; 185 | unsafe { 186 | gl.delete_program(self.program); 187 | gl.delete_vertex_array(self.vertex_array); 188 | } 189 | } 190 | 191 | fn paint(&self, gl: &glow::Context, angle: f32) { 192 | use glow::HasContext as _; 193 | unsafe { 194 | gl.use_program(Some(self.program)); 195 | gl.uniform_1_f32( 196 | gl.get_uniform_location(self.program, "u_angle").as_ref(), 197 | angle, 198 | ); 199 | gl.bind_vertex_array(Some(self.vertex_array)); 200 | gl.draw_arrays(glow::TRIANGLES, 0, 3); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/apps/custom3d_wgpu.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroU64, sync::Arc}; 2 | 3 | use eframe::{ 4 | egui_wgpu::wgpu::util::DeviceExt, 5 | egui_wgpu::{self, wgpu}, 6 | }; 7 | 8 | pub struct Custom3d { 9 | angle: f32, 10 | } 11 | 12 | impl Custom3d { 13 | pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Option { 14 | // Get the WGPU render state from the eframe creation context. This can also be retrieved 15 | // from `eframe::Frame` when you don't have a `CreationContext` available. 16 | let wgpu_render_state = cc.wgpu_render_state.as_ref()?; 17 | 18 | let device = &wgpu_render_state.device; 19 | 20 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 21 | label: Some("custom3d"), 22 | source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()), 23 | }); 24 | 25 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 26 | label: Some("custom3d"), 27 | entries: &[wgpu::BindGroupLayoutEntry { 28 | binding: 0, 29 | visibility: wgpu::ShaderStages::VERTEX, 30 | ty: wgpu::BindingType::Buffer { 31 | ty: wgpu::BufferBindingType::Uniform, 32 | has_dynamic_offset: false, 33 | min_binding_size: NonZeroU64::new(16), 34 | }, 35 | count: None, 36 | }], 37 | }); 38 | 39 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 40 | label: Some("custom3d"), 41 | bind_group_layouts: &[&bind_group_layout], 42 | push_constant_ranges: &[], 43 | }); 44 | 45 | let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 46 | label: Some("custom3d"), 47 | layout: Some(&pipeline_layout), 48 | vertex: wgpu::VertexState { 49 | module: &shader, 50 | entry_point: "vs_main", 51 | buffers: &[], 52 | }, 53 | fragment: Some(wgpu::FragmentState { 54 | module: &shader, 55 | entry_point: "fs_main", 56 | targets: &[Some(wgpu_render_state.target_format.into())], 57 | }), 58 | primitive: wgpu::PrimitiveState::default(), 59 | depth_stencil: None, 60 | multisample: wgpu::MultisampleState::default(), 61 | multiview: None, 62 | }); 63 | 64 | let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 65 | label: Some("custom3d"), 66 | contents: bytemuck::cast_slice(&[0.0_f32; 4]), // 16 bytes aligned! 67 | // Mapping at creation (as done by the create_buffer_init utility) doesn't require us to to add the MAP_WRITE usage 68 | // (this *happens* to workaround this bug ) 69 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, 70 | }); 71 | 72 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 73 | label: Some("custom3d"), 74 | layout: &bind_group_layout, 75 | entries: &[wgpu::BindGroupEntry { 76 | binding: 0, 77 | resource: uniform_buffer.as_entire_binding(), 78 | }], 79 | }); 80 | 81 | // Because the graphics pipeline must have the same lifetime as the egui render pass, 82 | // instead of storing the pipeline in our `Custom3D` struct, we insert it into the 83 | // `paint_callback_resources` type map, which is stored alongside the render pass. 84 | wgpu_render_state 85 | .renderer 86 | .write() 87 | .paint_callback_resources 88 | .insert(TriangleRenderResources { 89 | pipeline, 90 | bind_group, 91 | uniform_buffer, 92 | }); 93 | 94 | Some(Self { angle: 0.0 }) 95 | } 96 | } 97 | 98 | impl eframe::App for Custom3d { 99 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 100 | egui::CentralPanel::default().show(ctx, |ui| { 101 | egui::ScrollArea::both() 102 | .auto_shrink([false; 2]) 103 | .show(ui, |ui| { 104 | ui.horizontal(|ui| { 105 | ui.spacing_mut().item_spacing.x = 0.0; 106 | ui.label("The triangle is being painted using "); 107 | ui.hyperlink_to("WGPU", "https://wgpu.rs"); 108 | ui.label(" (Portable Rust graphics API awesomeness)"); 109 | }); 110 | ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui."); 111 | 112 | egui::Frame::canvas(ui.style()).show(ui, |ui| { 113 | self.custom_painting(ui); 114 | }); 115 | ui.label("Drag to rotate!"); 116 | ui.add(egui_demo_lib::egui_github_link_file!()); 117 | }); 118 | }); 119 | } 120 | } 121 | 122 | impl Custom3d { 123 | fn custom_painting(&mut self, ui: &mut egui::Ui) { 124 | let (rect, response) = 125 | ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); 126 | 127 | self.angle += response.drag_delta().x * 0.01; 128 | 129 | // Clone locals so we can move them into the paint callback: 130 | let angle = self.angle; 131 | 132 | // The callback function for WGPU is in two stages: prepare, and paint. 133 | // 134 | // The prepare callback is called every frame before paint and is given access to the wgpu 135 | // Device and Queue, which can be used, for instance, to update buffers and uniforms before 136 | // rendering. 137 | // 138 | // You can use the main `CommandEncoder` that is passed-in, return an arbitrary number 139 | // of user-defined `CommandBuffer`s, or both. 140 | // The main command buffer, as well as all user-defined ones, will be submitted together 141 | // to the GPU in a single call. 142 | // 143 | // The paint callback is called after prepare and is given access to the render pass, which 144 | // can be used to issue draw commands. 145 | let cb = egui_wgpu::CallbackFn::new() 146 | .prepare(move |device, queue, _encoder, paint_callback_resources| { 147 | let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); 148 | resources.prepare(device, queue, angle); 149 | Vec::new() 150 | }) 151 | .paint(move |_info, render_pass, paint_callback_resources| { 152 | let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); 153 | resources.paint(render_pass); 154 | }); 155 | 156 | let callback = egui::PaintCallback { 157 | rect, 158 | callback: Arc::new(cb), 159 | }; 160 | 161 | ui.painter().add(callback); 162 | } 163 | } 164 | 165 | struct TriangleRenderResources { 166 | pipeline: wgpu::RenderPipeline, 167 | bind_group: wgpu::BindGroup, 168 | uniform_buffer: wgpu::Buffer, 169 | } 170 | 171 | impl TriangleRenderResources { 172 | fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) { 173 | // Update our uniform buffer with the angle from the UI 174 | queue.write_buffer( 175 | &self.uniform_buffer, 176 | 0, 177 | bytemuck::cast_slice(&[angle, 0.0, 0.0, 0.0]), 178 | ); 179 | } 180 | 181 | fn paint<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>) { 182 | // Draw our triangle! 183 | render_pass.set_pipeline(&self.pipeline); 184 | render_pass.set_bind_group(0, &self.bind_group, &[]); 185 | render_pass.draw(0..3, 0..1); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOut { 2 | @location(0) color: vec4, 3 | @builtin(position) position: vec4, 4 | }; 5 | 6 | struct Uniforms { 7 | @size(16) angle: f32, // pad to 16 bytes 8 | }; 9 | 10 | @group(0) @binding(0) 11 | var uniforms: Uniforms; 12 | 13 | var v_positions: array, 3> = array, 3>( 14 | vec2(0.0, 1.0), 15 | vec2(1.0, -1.0), 16 | vec2(-1.0, -1.0), 17 | ); 18 | 19 | var v_colors: array, 3> = array, 3>( 20 | vec4(1.0, 0.0, 0.0, 1.0), 21 | vec4(0.0, 1.0, 0.0, 1.0), 22 | vec4(0.0, 0.0, 1.0, 1.0), 23 | ); 24 | 25 | @vertex 26 | fn vs_main(@builtin(vertex_index) v_idx: u32) -> VertexOut { 27 | var out: VertexOut; 28 | 29 | out.position = vec4(v_positions[v_idx], 0.0, 1.0); 30 | out.position.x = out.position.x * cos(uniforms.angle); 31 | out.color = v_colors[v_idx]; 32 | 33 | return out; 34 | } 35 | 36 | @fragment 37 | fn fs_main(in: VertexOut) -> @location(0) vec4 { 38 | return in.color; 39 | } 40 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/apps/fractal_clock.rs: -------------------------------------------------------------------------------- 1 | use egui::{containers::*, widgets::*, *}; 2 | use std::f32::consts::TAU; 3 | 4 | #[derive(PartialEq)] 5 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 6 | #[cfg_attr(feature = "serde", serde(default))] 7 | pub struct FractalClock { 8 | paused: bool, 9 | time: f64, 10 | zoom: f32, 11 | start_line_width: f32, 12 | depth: usize, 13 | length_factor: f32, 14 | luminance_factor: f32, 15 | width_factor: f32, 16 | line_count: usize, 17 | } 18 | 19 | impl Default for FractalClock { 20 | fn default() -> Self { 21 | Self { 22 | paused: false, 23 | time: 0.0, 24 | zoom: 0.25, 25 | start_line_width: 2.5, 26 | depth: 9, 27 | length_factor: 0.8, 28 | luminance_factor: 0.8, 29 | width_factor: 0.9, 30 | line_count: 0, 31 | } 32 | } 33 | } 34 | 35 | impl FractalClock { 36 | pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option) { 37 | if !self.paused { 38 | self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time)); 39 | ui.ctx().request_repaint(); 40 | } 41 | 42 | let painter = Painter::new( 43 | ui.ctx().clone(), 44 | ui.layer_id(), 45 | ui.available_rect_before_wrap(), 46 | ); 47 | self.paint(&painter); 48 | // Make sure we allocate what we used (everything) 49 | ui.expand_to_include_rect(painter.clip_rect()); 50 | 51 | Frame::popup(ui.style()) 52 | .stroke(Stroke::NONE) 53 | .show(ui, |ui| { 54 | ui.set_max_width(270.0); 55 | CollapsingHeader::new("Settings") 56 | .show(ui, |ui| self.options_ui(ui, seconds_since_midnight)); 57 | }); 58 | } 59 | 60 | fn options_ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option) { 61 | if seconds_since_midnight.is_some() { 62 | ui.label(format!( 63 | "Local time: {:02}:{:02}:{:02}.{:03}", 64 | (self.time % (24.0 * 60.0 * 60.0) / 3600.0).floor(), 65 | (self.time % (60.0 * 60.0) / 60.0).floor(), 66 | (self.time % 60.0).floor(), 67 | (self.time % 1.0 * 100.0).floor() 68 | )); 69 | } else { 70 | ui.label("The fractal_clock clock is not showing the correct time"); 71 | }; 72 | ui.label(format!("Painted line count: {}", self.line_count)); 73 | 74 | ui.checkbox(&mut self.paused, "Paused"); 75 | ui.add(Slider::new(&mut self.zoom, 0.0..=1.0).text("zoom")); 76 | ui.add(Slider::new(&mut self.start_line_width, 0.0..=5.0).text("Start line width")); 77 | ui.add(Slider::new(&mut self.depth, 0..=14).text("depth")); 78 | ui.add(Slider::new(&mut self.length_factor, 0.0..=1.0).text("length factor")); 79 | ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor")); 80 | ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor")); 81 | 82 | egui::reset_button(ui, self); 83 | 84 | ui.hyperlink_to( 85 | "Inspired by a screensaver by Rob Mayoff", 86 | "http://www.dqd.com/~mayoff/programs/FractalClock/", 87 | ); 88 | ui.add(egui_demo_lib::egui_github_link_file!()); 89 | } 90 | 91 | fn paint(&mut self, painter: &Painter) { 92 | struct Hand { 93 | length: f32, 94 | angle: f32, 95 | vec: Vec2, 96 | } 97 | 98 | impl Hand { 99 | fn from_length_angle(length: f32, angle: f32) -> Self { 100 | Self { 101 | length, 102 | angle, 103 | vec: length * Vec2::angled(angle), 104 | } 105 | } 106 | } 107 | 108 | let angle_from_period = 109 | |period| TAU * (self.time.rem_euclid(period) / period) as f32 - TAU / 4.0; 110 | 111 | let hands = [ 112 | // Second hand: 113 | Hand::from_length_angle(self.length_factor, angle_from_period(60.0)), 114 | // Minute hand: 115 | Hand::from_length_angle(self.length_factor, angle_from_period(60.0 * 60.0)), 116 | // Hour hand: 117 | Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)), 118 | ]; 119 | 120 | let mut shapes: Vec = Vec::new(); 121 | 122 | let rect = painter.clip_rect(); 123 | let to_screen = emath::RectTransform::from_to( 124 | Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom), 125 | rect, 126 | ); 127 | 128 | let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| { 129 | let line = [to_screen * points[0], to_screen * points[1]]; 130 | 131 | // culling 132 | if rect.intersects(Rect::from_two_pos(line[0], line[1])) { 133 | shapes.push(Shape::line_segment(line, (width, color))); 134 | } 135 | }; 136 | 137 | let hand_rotations = [ 138 | hands[0].angle - hands[2].angle + TAU / 2.0, 139 | hands[1].angle - hands[2].angle + TAU / 2.0, 140 | ]; 141 | 142 | let hand_rotors = [ 143 | hands[0].length * emath::Rot2::from_angle(hand_rotations[0]), 144 | hands[1].length * emath::Rot2::from_angle(hand_rotations[1]), 145 | ]; 146 | 147 | #[derive(Clone, Copy)] 148 | struct Node { 149 | pos: Pos2, 150 | dir: Vec2, 151 | } 152 | 153 | let mut nodes = Vec::new(); 154 | 155 | let mut width = self.start_line_width; 156 | 157 | for (i, hand) in hands.iter().enumerate() { 158 | let center = pos2(0.0, 0.0); 159 | let end = center + hand.vec; 160 | paint_line([center, end], Color32::from_additive_luminance(255), width); 161 | if i < 2 { 162 | nodes.push(Node { 163 | pos: end, 164 | dir: hand.vec, 165 | }); 166 | } 167 | } 168 | 169 | let mut luminance = 0.7; // Start dimmer than main hands 170 | 171 | let mut new_nodes = Vec::new(); 172 | for _ in 0..self.depth { 173 | new_nodes.clear(); 174 | new_nodes.reserve(nodes.len() * 2); 175 | 176 | luminance *= self.luminance_factor; 177 | width *= self.width_factor; 178 | 179 | let luminance_u8 = (255.0 * luminance).round() as u8; 180 | if luminance_u8 == 0 { 181 | break; 182 | } 183 | 184 | for &rotor in &hand_rotors { 185 | for a in &nodes { 186 | let new_dir = rotor * a.dir; 187 | let b = Node { 188 | pos: a.pos + new_dir, 189 | dir: new_dir, 190 | }; 191 | paint_line( 192 | [a.pos, b.pos], 193 | Color32::from_additive_luminance(luminance_u8), 194 | width, 195 | ); 196 | new_nodes.push(b); 197 | } 198 | } 199 | 200 | std::mem::swap(&mut nodes, &mut new_nodes); 201 | } 202 | self.line_count = shapes.len(); 203 | painter.extend(shapes); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/apps/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "glow", not(feature = "wgpu")))] 2 | mod custom3d_glow; 3 | 4 | #[cfg(feature = "wgpu")] 5 | mod custom3d_wgpu; 6 | 7 | mod fractal_clock; 8 | 9 | #[cfg(feature = "http")] 10 | mod http_app; 11 | 12 | #[cfg(all(feature = "glow", not(feature = "wgpu")))] 13 | pub use custom3d_glow::Custom3d; 14 | 15 | #[cfg(feature = "wgpu")] 16 | pub use custom3d_wgpu::Custom3d; 17 | 18 | pub use fractal_clock::FractalClock; 19 | 20 | #[cfg(feature = "http")] 21 | pub use http_app::HttpApp; 22 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/frame_history.rs: -------------------------------------------------------------------------------- 1 | use egui::util::History; 2 | 3 | pub struct FrameHistory { 4 | frame_times: History, 5 | } 6 | 7 | impl Default for FrameHistory { 8 | fn default() -> Self { 9 | let max_age: f32 = 1.0; 10 | let max_len = (max_age * 300.0).round() as usize; 11 | Self { 12 | frame_times: History::new(0..max_len, max_age), 13 | } 14 | } 15 | } 16 | 17 | impl FrameHistory { 18 | // Called first 19 | pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option) { 20 | let previous_frame_time = previous_frame_time.unwrap_or_default(); 21 | if let Some(latest) = self.frame_times.latest_mut() { 22 | *latest = previous_frame_time; // rewrite history now that we know 23 | } 24 | self.frame_times.add(now, previous_frame_time); // projected 25 | } 26 | 27 | pub fn mean_frame_time(&self) -> f32 { 28 | self.frame_times.average().unwrap_or_default() 29 | } 30 | 31 | pub fn fps(&self) -> f32 { 32 | 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() 33 | } 34 | 35 | pub fn ui(&mut self, ui: &mut egui::Ui) { 36 | ui.label(format!( 37 | "Mean CPU usage: {:.2} ms / frame", 38 | 1e3 * self.mean_frame_time() 39 | )) 40 | .on_hover_text( 41 | "Includes egui layout and tessellation time.\n\ 42 | Does not include GPU usage, nor overhead for sending data to GPU.", 43 | ); 44 | egui::warn_if_debug_build(ui); 45 | 46 | if !cfg!(target_arch = "wasm32") { 47 | egui::CollapsingHeader::new("📊 CPU usage history") 48 | .default_open(false) 49 | .show(ui, |ui| { 50 | self.graph(ui); 51 | }); 52 | } 53 | } 54 | 55 | fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response { 56 | use egui::*; 57 | 58 | ui.label("egui CPU usage history"); 59 | 60 | let history = &self.frame_times; 61 | 62 | // TODO(emilk): we should not use `slider_width` as default graph width. 63 | let height = ui.spacing().slider_width; 64 | let size = vec2(ui.available_size_before_wrap().x, height); 65 | let (rect, response) = ui.allocate_at_least(size, Sense::hover()); 66 | let style = ui.style().noninteractive(); 67 | 68 | let graph_top_cpu_usage = 0.010; 69 | let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0); 70 | let to_screen = emath::RectTransform::from_to(graph_rect, rect); 71 | 72 | let mut shapes = Vec::with_capacity(3 + 2 * history.len()); 73 | shapes.push(Shape::Rect(epaint::RectShape { 74 | rect, 75 | rounding: style.rounding, 76 | fill: ui.visuals().extreme_bg_color, 77 | stroke: ui.style().noninteractive().bg_stroke, 78 | })); 79 | 80 | let rect = rect.shrink(4.0); 81 | let color = ui.visuals().text_color(); 82 | let line_stroke = Stroke::new(1.0, color); 83 | 84 | if let Some(pointer_pos) = response.hover_pos() { 85 | let y = pointer_pos.y; 86 | shapes.push(Shape::line_segment( 87 | [pos2(rect.left(), y), pos2(rect.right(), y)], 88 | line_stroke, 89 | )); 90 | let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; 91 | let text = format!("{:.1} ms", 1e3 * cpu_usage); 92 | shapes.push(ui.fonts(|f| { 93 | Shape::text( 94 | f, 95 | pos2(rect.left(), y), 96 | egui::Align2::LEFT_BOTTOM, 97 | text, 98 | TextStyle::Monospace.resolve(ui.style()), 99 | color, 100 | ) 101 | })); 102 | } 103 | 104 | let circle_color = color; 105 | let radius = 2.0; 106 | let right_side_time = ui.input(|i| i.time); // Time at right side of screen 107 | 108 | for (time, cpu_usage) in history.iter() { 109 | let age = (right_side_time - time) as f32; 110 | let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage)); 111 | 112 | shapes.push(Shape::line_segment( 113 | [pos2(pos.x, rect.bottom()), pos], 114 | line_stroke, 115 | )); 116 | 117 | if cpu_usage < graph_top_cpu_usage { 118 | shapes.push(Shape::circle_filled(pos, radius, circle_color)); 119 | } 120 | } 121 | 122 | ui.painter().extend(shapes); 123 | 124 | response 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Demo app for egui 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | mod apps; 5 | mod backend_panel; 6 | mod frame_history; 7 | mod wrap_app; 8 | 9 | pub use wrap_app::WrapApp; 10 | 11 | /// Time of day as seconds since midnight. Used for clock in demo app. 12 | pub(crate) fn seconds_since_midnight() -> f64 { 13 | use chrono::Timelike; 14 | let time = chrono::Local::now().time(); 15 | time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) 16 | } 17 | 18 | // ---------------------------------------------------------------------------- 19 | 20 | #[cfg(target_arch = "wasm32")] 21 | mod web; 22 | 23 | #[cfg(target_arch = "wasm32")] 24 | pub use web::*; 25 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Demo app for egui 2 | 3 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release 4 | 5 | // When compiling natively: 6 | fn main() -> Result<(), eframe::Error> { 7 | { 8 | // Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206) 9 | let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); 10 | for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] { 11 | if !rust_log.contains(&format!("{loud_crate}=")) { 12 | rust_log += &format!(",{loud_crate}=warn"); 13 | } 14 | } 15 | std::env::set_var("RUST_LOG", rust_log); 16 | } 17 | 18 | env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). 19 | 20 | let options = eframe::NativeOptions { 21 | drag_and_drop_support: true, 22 | 23 | initial_window_size: Some([1280.0, 1024.0].into()), 24 | 25 | #[cfg(feature = "wgpu")] 26 | renderer: eframe::Renderer::Wgpu, 27 | 28 | ..Default::default() 29 | }; 30 | eframe::run_native( 31 | "egui demo app", 32 | options, 33 | Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))), 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /crates/egui_demo_app/src/web.rs: -------------------------------------------------------------------------------- 1 | use eframe::wasm_bindgen::{self, prelude::*}; 2 | 3 | use crate::WrapApp; 4 | 5 | /// Our handle to the web app from JavaScript. 6 | #[derive(Clone)] 7 | #[wasm_bindgen] 8 | pub struct WebHandle { 9 | runner: eframe::WebRunner, 10 | } 11 | 12 | #[wasm_bindgen] 13 | impl WebHandle { 14 | /// Installs a panic hook, then returns. 15 | #[allow(clippy::new_without_default)] 16 | #[wasm_bindgen(constructor)] 17 | pub fn new() -> Self { 18 | // Redirect [`log`] message to `console.log` and friends: 19 | eframe::WebLogger::init(log::LevelFilter::Debug).ok(); 20 | 21 | Self { 22 | runner: eframe::WebRunner::new(), 23 | } 24 | } 25 | 26 | /// Call this once from JavaScript to start your app. 27 | #[wasm_bindgen] 28 | pub async fn start(&self, canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { 29 | self.runner 30 | .start( 31 | canvas_id, 32 | eframe::WebOptions::default(), 33 | Box::new(|cc| Box::new(WrapApp::new(cc))), 34 | ) 35 | .await 36 | } 37 | 38 | #[wasm_bindgen] 39 | pub fn destroy(&self) { 40 | self.runner.destroy(); 41 | } 42 | 43 | /// Example on how to call into your app from JavaScript. 44 | #[wasm_bindgen] 45 | pub fn example(&self) { 46 | if let Some(_app) = self.runner.app_mut::() { 47 | // _app.example(); 48 | } 49 | } 50 | 51 | /// The JavaScript can check whether or not your app has crashed: 52 | #[wasm_bindgen] 53 | pub fn has_panicked(&self) -> bool { 54 | self.runner.has_panicked() 55 | } 56 | 57 | #[wasm_bindgen] 58 | pub fn panic_message(&self) -> Option { 59 | self.runner.panic_summary().map(|s| s.message()) 60 | } 61 | 62 | #[wasm_bindgen] 63 | pub fn panic_callstack(&self) -> Option { 64 | self.runner.panic_summary().map(|s| s.callstack()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/egui_glow/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for egui_glow 2 | 3 | ## \[0.23.0] 4 | 5 | - [`b41f29b4`](https://github.com/tauri-apps/egui/commit/b41f29b4dd80dfef2ed65ce8acc98977fc6f63aa) Upgraded to use egui 0.22.0 6 | 7 | All notable changes to the `egui_glow` integration will be noted in this file. 8 | 9 | ## Unreleased 10 | 11 | ## 0.22.0 - 2023-05-23 12 | 13 | ## 0.21.0 - 2023-02-08 14 | 15 | - Update to `glow` 0.12 ([#2695](https://github.com/emilk/egui/pull/2695)). 16 | - Remove the `screen_reader` feature ([#2669](https://github.com/emilk/egui/pull/2669)). 17 | 18 | ## 0.20.1 - 2022-12-11 19 | 20 | - Fix [docs.rs](https://docs.rs/egui_glow) build ([#2420](https://github.com/emilk/egui/pull/2420)). 21 | 22 | ## 0.20.0 - 2022-12-08 23 | 24 | - Allow empty textures. 25 | - Added `shader_version` variable on `EguiGlow::new` for easier cross compiling on different OpenGL | ES targets ([#1993](https://github.com/emilk/egui/pull/1993)). 26 | 27 | ## 0.19.0 - 2022-08-20 28 | 29 | - MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). 30 | - `EguiGlow::new` now takes an `EventLoopWindowTarget` instead of a `winit::Window` ([#1634](https://github.com/emilk/egui/pull/1634)). 31 | - Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)). 32 | - Fixed `glClear` on WebGL1 ([#1658](https://github.com/emilk/egui/pull/1658)). 33 | - Add `Painter::intermediate_fbo` which tells callbacks where to render. This is only needed if the callbacks use their own FBO:s and need to know what to restore to. 34 | 35 | ## 0.18.1 - 2022-05-05 36 | 37 | - Remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)). 38 | 39 | ## 0.18.0 - 2022-04-30 40 | 41 | - Improved logging on rendering failures. 42 | - Added new `NativeOptions`: `vsync`, `multisampling`, `depth_buffer`, `stencil_buffer`. 43 | - Fixed potential scale bug when DPI scaling changes (e.g. when dragging a window between different displays) ([#1441](https://github.com/emilk/egui/pull/1441)). 44 | - MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)). 45 | - `clipboard`, `links`, `winit` are now all opt-in features ([#1467](https://github.com/emilk/egui/pull/1467)). 46 | - Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)). 47 | - Removed the features `dark-light`, `default_fonts` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)). 48 | 49 | ## 0.17.0 - 2022-02-22 50 | 51 | - `EguiGlow::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlow::paint` ([#1110](https://github.com/emilk/egui/pull/1110)). 52 | - Added `set_texture_filter` method to `Painter` ([#1041](https://github.com/emilk/egui/pull/1041)). 53 | - Fixed failure to run in Chrome ([#1092](https://github.com/emilk/egui/pull/1092)). 54 | - `EguiGlow::new` and `EguiGlow::paint` now takes `&winit::Window` ([#1151](https://github.com/emilk/egui/pull/1151)). 55 | - Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)). 56 | 57 | ## 0.16.0 - 2021-12-29 58 | 59 | - Made winit/glutin an optional dependency ([#868](https://github.com/emilk/egui/pull/868)). 60 | - Simplified `EguiGlow` interface ([#871](https://github.com/emilk/egui/pull/871)). 61 | - Removed `EguiGlow::is_quit_event` ([#881](https://github.com/emilk/egui/pull/881)). 62 | - Updated `glutin` to 0.28 ([#930](https://github.com/emilk/egui/pull/930)). 63 | - Changed the `Painter` interface slightly ([#999](https://github.com/emilk/egui/pull/999)). 64 | 65 | ## 0.15.0 - 2021-10-24 66 | 67 | `egui_glow` has been newly created, with feature parity to `egui_glium`. 68 | 69 | As `glow` is a set of lower-level bindings to OpenGL, this crate is potentially less stable than `egui_glium`, 70 | but hopefully this will one day replace `egui_glium` as the default backend for `eframe`. 71 | -------------------------------------------------------------------------------- /crates/egui_glow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_glow_tao" 3 | version = "0.23.0" 4 | authors = [ "Emil Ernerfeldt " ] 5 | description = "Bindings for using egui natively using the glow library" 6 | edition = "2021" 7 | rust-version = "1.65" 8 | homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glow" 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/emilk/egui/tree/master/crates/egui_glow" 12 | categories = [ "gui", "game-development" ] 13 | keywords = [ "glow", "egui", "gui", "gamedev" ] 14 | include = [ 15 | "../LICENSE-APACHE", 16 | "../LICENSE-MIT", 17 | "**/*.rs", 18 | "Cargo.toml", 19 | "src/shader/*.glsl" 20 | ] 21 | 22 | [package.metadata.docs.rs] 23 | all-features = true 24 | 25 | [features] 26 | default = [ ] 27 | clipboard = [ "egui-winit?/clipboard" ] 28 | links = [ "egui-winit?/links" ] 29 | puffin = [ "dep:puffin", "egui-winit?/puffin" ] 30 | winit = [ "egui-winit" ] 31 | 32 | [dependencies] 33 | egui = { version = "0.22.0", default-features = false, features = [ "bytemuck" ] } 34 | bytemuck = "1.7" 35 | glow = "0.12" 36 | log = { version = "0.4", features = [ "std" ] } 37 | memoffset = "0.6" 38 | document-features = { version = "0.2", optional = true } 39 | 40 | [target."cfg(not(target_arch = \"wasm32\"))".dependencies] 41 | egui-winit = { package = "egui-tao", version = "0.23.0", path = "../egui-winit", optional = true, default-features = false } 42 | puffin = { version = "0.15", optional = true } 43 | 44 | [target."cfg(target_arch = \"wasm32\")".dependencies] 45 | web-sys = { version = "0.3", features = [ "console" ] } 46 | wasm-bindgen = { version = "0.2" } 47 | 48 | [dev-dependencies] 49 | glutin = "0.30" 50 | raw-window-handle = "0.5.0" 51 | glutin-winit = { package = "glutin_tao", version = "0.33.0", git = "https://github.com/tauri-apps/glutin", branch = "0.31" } 52 | 53 | [[example]] 54 | name = "pure_glow" 55 | required-features = [ "winit", "egui/default_fonts" ] 56 | -------------------------------------------------------------------------------- /crates/egui_glow/README.md: -------------------------------------------------------------------------------- 1 | # egui_glow_tao 2 | 3 | [![Latest version](https://img.shields.io/crates/v/egui_glow_tao.svg)](https://crates.io/crates/egui_glow_tao) 4 | [![Documentation](https://docs.rs/egui_glow_tao/badge.svg)](https://docs.rs/egui_glow_tao) 5 | ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 6 | ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) 7 | 8 | This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [glow](https://crates.io/crates/glow) which allows you to: 9 | 10 | - Render egui using glow on both native and web. 11 | - Write cross platform native egui apps (with the `tao` feature). 12 | 13 | To write web apps using `glow` you can use [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) (which uses `egui_glow_tao` for rendering). 14 | 15 | To use on Linux, first run: 16 | 17 | ``` 18 | sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev 19 | ``` 20 | 21 | This crate optionally depends on [`egui-tao`](https://github.com/emilk/egui/tree/master/crates/egui-tao). 22 | 23 | Text the example with: 24 | 25 | ```sh 26 | cargo run -p egui_glow --example pure_glow --features=winit,egui/default_fonts 27 | ``` 28 | -------------------------------------------------------------------------------- /crates/egui_glow/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [`egui`] bindings for [`glow`](https://github.com/grovesNL/glow). 2 | //! 3 | //! The main types you want to look are are [`Painter`]. 4 | //! 5 | //! If you are writing an app, you may want to look at [`eframe`](https://docs.rs/eframe) instead. 6 | //! 7 | //! ## Feature flags 8 | #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] 9 | //! 10 | 11 | #![allow(clippy::float_cmp)] 12 | #![allow(clippy::manual_range_contains)] 13 | 14 | pub mod painter; 15 | pub use glow; 16 | pub use painter::{CallbackFn, Painter}; 17 | mod misc_util; 18 | mod shader_version; 19 | mod vao; 20 | 21 | pub use shader_version::ShaderVersion; 22 | 23 | #[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] 24 | pub mod winit; 25 | #[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] 26 | pub use winit::*; 27 | 28 | /// Check for OpenGL error and report it using `log::error`. 29 | /// 30 | /// Only active in debug builds! 31 | /// 32 | /// ``` no_run 33 | /// # let glow_context = todo!(); 34 | /// use egui_glow_tao::check_for_gl_error; 35 | /// check_for_gl_error!(glow_context); 36 | /// check_for_gl_error!(glow_context, "during painting"); 37 | /// ``` 38 | #[macro_export] 39 | macro_rules! check_for_gl_error { 40 | ($gl: expr) => {{ 41 | if cfg!(debug_assertions) { 42 | $crate::check_for_gl_error_impl($gl, file!(), line!(), "") 43 | } 44 | }}; 45 | ($gl: expr, $context: literal) => {{ 46 | if cfg!(debug_assertions) { 47 | $crate::check_for_gl_error_impl($gl, file!(), line!(), $context) 48 | } 49 | }}; 50 | } 51 | 52 | /// Check for OpenGL error and report it using `log::error`. 53 | /// 54 | /// WARNING: slow! Only use during setup! 55 | /// 56 | /// ``` no_run 57 | /// # let glow_context = todo!(); 58 | /// use egui_glow_tao::check_for_gl_error_even_in_release; 59 | /// check_for_gl_error_even_in_release!(glow_context); 60 | /// check_for_gl_error_even_in_release!(glow_context, "during painting"); 61 | /// ``` 62 | #[macro_export] 63 | macro_rules! check_for_gl_error_even_in_release { 64 | ($gl: expr) => {{ 65 | $crate::check_for_gl_error_impl($gl, file!(), line!(), "") 66 | }}; 67 | ($gl: expr, $context: literal) => {{ 68 | $crate::check_for_gl_error_impl($gl, file!(), line!(), $context) 69 | }}; 70 | } 71 | 72 | #[doc(hidden)] 73 | pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, context: &str) { 74 | use glow::HasContext as _; 75 | #[allow(unsafe_code)] 76 | let error_code = unsafe { gl.get_error() }; 77 | if error_code != glow::NO_ERROR { 78 | let error_str = match error_code { 79 | glow::INVALID_ENUM => "GL_INVALID_ENUM", 80 | glow::INVALID_VALUE => "GL_INVALID_VALUE", 81 | glow::INVALID_OPERATION => "GL_INVALID_OPERATION", 82 | glow::STACK_OVERFLOW => "GL_STACK_OVERFLOW", 83 | glow::STACK_UNDERFLOW => "GL_STACK_UNDERFLOW", 84 | glow::OUT_OF_MEMORY => "GL_OUT_OF_MEMORY", 85 | glow::INVALID_FRAMEBUFFER_OPERATION => "GL_INVALID_FRAMEBUFFER_OPERATION", 86 | glow::CONTEXT_LOST => "GL_CONTEXT_LOST", 87 | 0x8031 => "GL_TABLE_TOO_LARGE1", 88 | 0x9242 => "CONTEXT_LOST_WEBGL", 89 | _ => "", 90 | }; 91 | 92 | if context.is_empty() { 93 | log::error!( 94 | "GL error, at {}:{}: {} (0x{:X}). Please file a bug at https://github.com/emilk/egui/issues", 95 | file, 96 | line, 97 | error_str, 98 | error_code, 99 | ); 100 | } else { 101 | log::error!( 102 | "GL error, at {}:{} ({}): {} (0x{:X}). Please file a bug at https://github.com/emilk/egui/issues", 103 | file, 104 | line, 105 | context, 106 | error_str, 107 | error_code, 108 | ); 109 | } 110 | } 111 | } 112 | 113 | // --------------------------------------------------------------------------- 114 | 115 | /// Profiling macro for feature "puffin" 116 | macro_rules! profile_function { 117 | ($($arg: tt)*) => { 118 | #[cfg(feature = "puffin")] 119 | #[cfg(not(target_arch = "wasm32"))] 120 | puffin::profile_function!($($arg)*); 121 | }; 122 | } 123 | pub(crate) use profile_function; 124 | 125 | /// Profiling macro for feature "puffin" 126 | macro_rules! profile_scope { 127 | ($($arg: tt)*) => { 128 | #[cfg(feature = "puffin")] 129 | #[cfg(not(target_arch = "wasm32"))] 130 | puffin::profile_scope!($($arg)*); 131 | }; 132 | } 133 | pub(crate) use profile_scope; 134 | -------------------------------------------------------------------------------- /crates/egui_glow/src/misc_util.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] 2 | 3 | use glow::HasContext as _; 4 | 5 | pub(crate) unsafe fn compile_shader( 6 | gl: &glow::Context, 7 | shader_type: u32, 8 | source: &str, 9 | ) -> Result { 10 | let shader = gl.create_shader(shader_type)?; 11 | 12 | gl.shader_source(shader, source); 13 | 14 | gl.compile_shader(shader); 15 | 16 | if gl.get_shader_compile_status(shader) { 17 | Ok(shader) 18 | } else { 19 | Err(gl.get_shader_info_log(shader)) 20 | } 21 | } 22 | 23 | pub(crate) unsafe fn link_program<'a, T: IntoIterator>( 24 | gl: &glow::Context, 25 | shaders: T, 26 | ) -> Result { 27 | let program = gl.create_program()?; 28 | 29 | for shader in shaders { 30 | gl.attach_shader(program, *shader); 31 | } 32 | 33 | gl.link_program(program); 34 | 35 | if gl.get_program_link_status(program) { 36 | Ok(program) 37 | } else { 38 | Err(gl.get_program_info_log(program)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/egui_glow/src/shader/fragment.glsl: -------------------------------------------------------------------------------- 1 | #ifdef GL_ES 2 | precision mediump float; 3 | #endif 4 | 5 | uniform sampler2D u_sampler; 6 | 7 | #if NEW_SHADER_INTERFACE 8 | in vec4 v_rgba_in_gamma; 9 | in vec2 v_tc; 10 | out vec4 f_color; 11 | // a dirty hack applied to support webGL2 12 | #define gl_FragColor f_color 13 | #define texture2D texture 14 | #else 15 | varying vec4 v_rgba_in_gamma; 16 | varying vec2 v_tc; 17 | #endif 18 | 19 | // 0-1 sRGB gamma from 0-1 linear 20 | vec3 srgb_gamma_from_linear(vec3 rgb) { 21 | bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); 22 | vec3 lower = rgb * vec3(12.92); 23 | vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); 24 | return mix(higher, lower, vec3(cutoff)); 25 | } 26 | 27 | // 0-1 sRGBA gamma from 0-1 linear 28 | vec4 srgba_gamma_from_linear(vec4 rgba) { 29 | return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a); 30 | } 31 | 32 | void main() { 33 | #if SRGB_TEXTURES 34 | vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc)); 35 | #else 36 | vec4 texture_in_gamma = texture2D(u_sampler, v_tc); 37 | #endif 38 | 39 | // We multiply the colors in gamma space, because that's the only way to get text to look right. 40 | gl_FragColor = v_rgba_in_gamma * texture_in_gamma; 41 | } 42 | -------------------------------------------------------------------------------- /crates/egui_glow/src/shader/vertex.glsl: -------------------------------------------------------------------------------- 1 | #if NEW_SHADER_INTERFACE 2 | #define I in 3 | #define O out 4 | #define V(x) x 5 | #else 6 | #define I attribute 7 | #define O varying 8 | #define V(x) vec3(x) 9 | #endif 10 | 11 | #ifdef GL_ES 12 | precision mediump float; 13 | #endif 14 | 15 | uniform vec2 u_screen_size; 16 | I vec2 a_pos; 17 | I vec4 a_srgba; // 0-255 sRGB 18 | I vec2 a_tc; 19 | O vec4 v_rgba_in_gamma; 20 | O vec2 v_tc; 21 | 22 | void main() { 23 | gl_Position = vec4( 24 | 2.0 * a_pos.x / u_screen_size.x - 1.0, 25 | 1.0 - 2.0 * a_pos.y / u_screen_size.y, 26 | 0.0, 27 | 1.0); 28 | v_rgba_in_gamma = a_srgba / 255.0; 29 | v_tc = a_tc; 30 | } 31 | -------------------------------------------------------------------------------- /crates/egui_glow/src/shader_version.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] 2 | 3 | use std::convert::TryInto; 4 | 5 | /// Helper for parsing and interpreting the OpenGL shader version. 6 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 7 | #[allow(dead_code)] 8 | pub enum ShaderVersion { 9 | Gl120, 10 | 11 | /// OpenGL 1.4 or later 12 | Gl140, 13 | 14 | /// e.g. WebGL1 15 | Es100, 16 | 17 | /// e.g. WebGL2 18 | Es300, 19 | } 20 | 21 | impl ShaderVersion { 22 | pub fn get(gl: &glow::Context) -> Self { 23 | use glow::HasContext as _; 24 | let shading_lang_string = 25 | unsafe { gl.get_parameter_string(glow::SHADING_LANGUAGE_VERSION) }; 26 | let shader_version = Self::parse(&shading_lang_string); 27 | log::debug!( 28 | "Shader version: {:?} ({:?}).", 29 | shader_version, 30 | shading_lang_string 31 | ); 32 | shader_version 33 | } 34 | 35 | #[inline] 36 | pub(crate) fn parse(glsl_ver: &str) -> Self { 37 | let start = glsl_ver.find(|c| char::is_ascii_digit(&c)).unwrap(); 38 | let es = glsl_ver[..start].contains(" ES "); 39 | let ver = glsl_ver[start..] 40 | .split_once(' ') 41 | .map_or(&glsl_ver[start..], |x| x.0); 42 | let [maj, min]: [u8; 2] = ver 43 | .splitn(3, '.') 44 | .take(2) 45 | .map(|x| x.parse().unwrap_or_default()) 46 | .collect::>() 47 | .try_into() 48 | .unwrap(); 49 | if es { 50 | if maj >= 3 { 51 | Self::Es300 52 | } else { 53 | Self::Es100 54 | } 55 | } else if maj > 1 || (maj == 1 && min >= 40) { 56 | Self::Gl140 57 | } else { 58 | Self::Gl120 59 | } 60 | } 61 | 62 | /// Goes on top of the shader. 63 | pub fn version_declaration(&self) -> &'static str { 64 | match self { 65 | Self::Gl120 => "#version 120\n", 66 | Self::Gl140 => "#version 140\n", 67 | Self::Es100 => "#version 100\n", 68 | Self::Es300 => "#version 300 es\n", 69 | } 70 | } 71 | 72 | /// If true, use `in/out`. If `false`, use `varying` and `gl_FragColor`. 73 | pub fn is_new_shader_interface(&self) -> bool { 74 | match self { 75 | Self::Gl120 | Self::Es100 => false, 76 | Self::Es300 | Self::Gl140 => true, 77 | } 78 | } 79 | 80 | pub fn is_embedded(&self) -> bool { 81 | match self { 82 | Self::Gl120 | Self::Gl140 => false, 83 | Self::Es100 | Self::Es300 => true, 84 | } 85 | } 86 | } 87 | 88 | #[test] 89 | fn test_shader_version() { 90 | use ShaderVersion::{Es100, Es300, Gl120, Gl140}; 91 | for (s, v) in [ 92 | ("1.2 OpenGL foo bar", Gl120), 93 | ("3.0", Gl140), 94 | ("0.0", Gl120), 95 | ("OpenGL ES GLSL 3.00 (WebGL2)", Es300), 96 | ("OpenGL ES GLSL 1.00 (WebGL)", Es100), 97 | ("OpenGL ES GLSL ES 1.00 foo bar", Es100), 98 | ("WebGL GLSL ES 3.00 foo bar", Es300), 99 | ("WebGL GLSL ES 3.00", Es300), 100 | ("WebGL GLSL ES 1.0 foo bar", Es100), 101 | ] { 102 | assert_eq!(ShaderVersion::parse(s), v); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/egui_glow/src/vao.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] 2 | 3 | use glow::HasContext as _; 4 | 5 | use crate::check_for_gl_error; 6 | 7 | // ---------------------------------------------------------------------------- 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct BufferInfo { 11 | pub location: u32, // 12 | pub vector_size: i32, 13 | pub data_type: u32, //GL_FLOAT,GL_UNSIGNED_BYTE 14 | pub normalized: bool, 15 | pub stride: i32, 16 | pub offset: i32, 17 | } 18 | 19 | // ---------------------------------------------------------------------------- 20 | 21 | /// Wrapper around either Emulated VAO or GL's VAO. 22 | pub(crate) struct VertexArrayObject { 23 | // If `None`, we emulate VAO:s. 24 | vao: Option, 25 | vbo: glow::Buffer, 26 | buffer_infos: Vec, 27 | } 28 | 29 | impl VertexArrayObject { 30 | #[allow(clippy::needless_pass_by_value)] // false positive 31 | pub(crate) unsafe fn new( 32 | gl: &glow::Context, 33 | vbo: glow::Buffer, 34 | buffer_infos: Vec, 35 | ) -> Self { 36 | let vao = if supports_vao(gl) { 37 | let vao = gl.create_vertex_array().unwrap(); 38 | check_for_gl_error!(gl, "create_vertex_array"); 39 | 40 | // Store state in the VAO: 41 | gl.bind_vertex_array(Some(vao)); 42 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); 43 | 44 | for attribute in &buffer_infos { 45 | gl.vertex_attrib_pointer_f32( 46 | attribute.location, 47 | attribute.vector_size, 48 | attribute.data_type, 49 | attribute.normalized, 50 | attribute.stride, 51 | attribute.offset, 52 | ); 53 | check_for_gl_error!(gl, "vertex_attrib_pointer_f32"); 54 | gl.enable_vertex_attrib_array(attribute.location); 55 | check_for_gl_error!(gl, "enable_vertex_attrib_array"); 56 | } 57 | 58 | gl.bind_vertex_array(None); 59 | 60 | Some(vao) 61 | } else { 62 | log::debug!("VAO not supported"); 63 | None 64 | }; 65 | 66 | Self { 67 | vao, 68 | vbo, 69 | buffer_infos, 70 | } 71 | } 72 | 73 | pub(crate) unsafe fn bind(&self, gl: &glow::Context) { 74 | if let Some(vao) = self.vao { 75 | gl.bind_vertex_array(Some(vao)); 76 | check_for_gl_error!(gl, "bind_vertex_array"); 77 | } else { 78 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo)); 79 | check_for_gl_error!(gl, "bind_buffer"); 80 | 81 | for attribute in &self.buffer_infos { 82 | gl.vertex_attrib_pointer_f32( 83 | attribute.location, 84 | attribute.vector_size, 85 | attribute.data_type, 86 | attribute.normalized, 87 | attribute.stride, 88 | attribute.offset, 89 | ); 90 | check_for_gl_error!(gl, "vertex_attrib_pointer_f32"); 91 | gl.enable_vertex_attrib_array(attribute.location); 92 | check_for_gl_error!(gl, "enable_vertex_attrib_array"); 93 | } 94 | } 95 | } 96 | 97 | pub(crate) unsafe fn unbind(&self, gl: &glow::Context) { 98 | if self.vao.is_some() { 99 | gl.bind_vertex_array(None); 100 | } else { 101 | gl.bind_buffer(glow::ARRAY_BUFFER, None); 102 | for attribute in &self.buffer_infos { 103 | gl.disable_vertex_attrib_array(attribute.location); 104 | } 105 | } 106 | } 107 | } 108 | 109 | // ---------------------------------------------------------------------------- 110 | 111 | fn supports_vao(gl: &glow::Context) -> bool { 112 | const WEBGL_PREFIX: &str = "WebGL "; 113 | const OPENGL_ES_PREFIX: &str = "OpenGL ES "; 114 | 115 | let version_string = unsafe { gl.get_parameter_string(glow::VERSION) }; 116 | log::debug!("GL version: {:?}.", version_string); 117 | 118 | // Examples: 119 | // * "WebGL 2.0 (OpenGL ES 3.0 Chromium)" 120 | // * "WebGL 2.0" 121 | 122 | if let Some(pos) = version_string.rfind(WEBGL_PREFIX) { 123 | let version_str = &version_string[pos + WEBGL_PREFIX.len()..]; 124 | if version_str.contains("1.0") { 125 | // need to test OES_vertex_array_object . 126 | let supported_extensions = gl.supported_extensions(); 127 | log::debug!("Supported OpenGL extensions: {:?}", supported_extensions); 128 | supported_extensions.contains("OES_vertex_array_object") 129 | || supported_extensions.contains("GL_OES_vertex_array_object") 130 | } else { 131 | true 132 | } 133 | } else if version_string.contains(OPENGL_ES_PREFIX) { 134 | // glow targets es2.0+ so we don't concern about OpenGL ES-CM,OpenGL ES-CL 135 | if version_string.contains("2.0") { 136 | // need to test OES_vertex_array_object . 137 | let supported_extensions = gl.supported_extensions(); 138 | log::debug!("Supported OpenGL extensions: {:?}", supported_extensions); 139 | supported_extensions.contains("OES_vertex_array_object") 140 | || supported_extensions.contains("GL_OES_vertex_array_object") 141 | } else { 142 | true 143 | } 144 | } else { 145 | // from OpenGL 3 vao into core 146 | if version_string.starts_with('2') { 147 | // I found APPLE_vertex_array_object , GL_ATI_vertex_array_object ,ARB_vertex_array_object 148 | // but APPLE's and ATI's very old extension. 149 | let supported_extensions = gl.supported_extensions(); 150 | log::debug!("Supported OpenGL extensions: {:?}", supported_extensions); 151 | supported_extensions.contains("ARB_vertex_array_object") 152 | || supported_extensions.contains("GL_ARB_vertex_array_object") 153 | } else { 154 | true 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /crates/egui_glow/src/winit.rs: -------------------------------------------------------------------------------- 1 | use crate::shader_version::ShaderVersion; 2 | pub use egui_winit; 3 | use egui_winit::winit; 4 | pub use egui_winit::EventResponse; 5 | 6 | /// Use [`egui`] from a [`glow`] app based on [`winit`]. 7 | pub struct EguiGlow { 8 | pub egui_ctx: egui::Context, 9 | pub egui_winit: egui_winit::State, 10 | pub painter: crate::Painter, 11 | 12 | shapes: Vec, 13 | textures_delta: egui::TexturesDelta, 14 | } 15 | 16 | impl EguiGlow { 17 | /// For automatic shader version detection set `shader_version` to `None`. 18 | pub fn new( 19 | event_loop: &winit::event_loop::EventLoopWindowTarget, 20 | gl: std::sync::Arc, 21 | shader_version: Option, 22 | ) -> Self { 23 | let painter = crate::Painter::new(gl, "", shader_version) 24 | .map_err(|err| { 25 | log::error!("error occurred in initializing painter:\n{err}"); 26 | }) 27 | .unwrap(); 28 | 29 | Self { 30 | egui_ctx: Default::default(), 31 | egui_winit: egui_winit::State::new(event_loop), 32 | painter, 33 | shapes: Default::default(), 34 | textures_delta: Default::default(), 35 | } 36 | } 37 | 38 | pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) -> EventResponse { 39 | self.egui_winit.on_event(&self.egui_ctx, event) 40 | } 41 | 42 | /// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events. 43 | /// 44 | /// Call [`Self::paint`] later to paint. 45 | pub fn run( 46 | &mut self, 47 | window: &winit::window::Window, 48 | run_ui: impl FnMut(&egui::Context), 49 | ) -> std::time::Duration { 50 | let raw_input = self.egui_winit.take_egui_input(window); 51 | let egui::FullOutput { 52 | platform_output, 53 | repaint_after, 54 | textures_delta, 55 | shapes, 56 | } = self.egui_ctx.run(raw_input, run_ui); 57 | 58 | self.egui_winit 59 | .handle_platform_output(window, &self.egui_ctx, platform_output); 60 | 61 | self.shapes = shapes; 62 | self.textures_delta.append(textures_delta); 63 | repaint_after 64 | } 65 | 66 | /// Paint the results of the last call to [`Self::run`]. 67 | pub fn paint(&mut self, window: &winit::window::Window) { 68 | let shapes = std::mem::take(&mut self.shapes); 69 | let mut textures_delta = std::mem::take(&mut self.textures_delta); 70 | 71 | for (id, image_delta) in textures_delta.set { 72 | self.painter.set_texture(id, &image_delta); 73 | } 74 | 75 | let clipped_primitives = self.egui_ctx.tessellate(shapes); 76 | let dimensions: [u32; 2] = window.inner_size().into(); 77 | self.painter.paint_primitives( 78 | dimensions, 79 | self.egui_ctx.pixels_per_point(), 80 | &clipped_primitives, 81 | ); 82 | 83 | for id in textures_delta.free.drain(..) { 84 | self.painter.free_texture(id); 85 | } 86 | } 87 | 88 | /// Call to release the allocated graphics resources. 89 | pub fn destroy(&mut self) { 90 | self.painter.destroy(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # https://embarkstudios.github.io/cargo-deny/ 2 | 3 | # Note: running just `cargo deny check` without a `--target` can result in 4 | # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 5 | targets = [ 6 | { triple = "aarch64-apple-darwin" }, 7 | { triple = "i686-pc-windows-gnu" }, 8 | { triple = "i686-pc-windows-msvc" }, 9 | { triple = "i686-unknown-linux-gnu" }, 10 | { triple = "wasm32-unknown-unknown" }, 11 | { triple = "x86_64-apple-darwin" }, 12 | { triple = "x86_64-pc-windows-gnu" }, 13 | { triple = "x86_64-pc-windows-msvc" }, 14 | { triple = "x86_64-unknown-linux-gnu" }, 15 | { triple = "x86_64-unknown-linux-musl" }, 16 | { triple = "x86_64-unknown-redox" }, 17 | ] 18 | 19 | [advisories] 20 | vulnerability = "deny" 21 | unmaintained = "warn" 22 | yanked = "deny" 23 | ignore = [ 24 | "RUSTSEC-2020-0071", # https://rustsec.org/advisories/RUSTSEC-2020-0071 - chrono/time: Potential segfault in the time crate 25 | ] 26 | 27 | [bans] 28 | multiple-versions = "deny" 29 | wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed 30 | deny = [ 31 | { name = "cmake" }, # Lord no 32 | { name = "openssl-sys" }, # prefer rustls 33 | { name = "openssl" }, # prefer rustls 34 | ] 35 | 36 | skip = [ 37 | { name = "arrayvec" }, # old version via tiny-skiaz 38 | { name = "libloading" }, # wgpu-hal itself depends on 0.8 while some of its dependencies, like ash and d3d12, depend on 0.7 39 | { name = "nix" }, # old version via winit 40 | { name = "redox_syscall" }, # old version via winit 41 | { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' 42 | { name = "tiny-skia" }, # winit uses a different version from egui_extras (TODO(emilk): update egui_extras!) 43 | { name = "ttf-parser" }, # different versions pulled in by ab_glyph and usvg 44 | { name = "wayland-sys" }, # old version via winit 45 | { name = "windows_x86_64_msvc" }, # old version via glutin 46 | { name = "windows-sys" }, # old version via glutin 47 | { name = "windows" }, # old version via accesskit 48 | ] 49 | skip-tree = [ 50 | { name = "criterion" }, # dev-dependency 51 | { name = "rfd" }, # example dependency 52 | ] 53 | 54 | 55 | [licenses] 56 | unlicensed = "deny" 57 | allow-osi-fsf-free = "neither" 58 | confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text 59 | copyleft = "deny" 60 | allow = [ 61 | "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html 62 | "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) 63 | "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) 64 | "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) 65 | "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained 66 | "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ 67 | "ISC", # https://tldrlegal.com/license/-isc-license 68 | "LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321 69 | "MIT", # https://tldrlegal.com/license/mit-license 70 | "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11 71 | "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html 72 | "OpenSSL", # https://www.openssl.org/source/license.html 73 | "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html 74 | "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) 75 | ] 76 | 77 | [[licenses.clarify]] 78 | name = "webpki" 79 | expression = "ISC" 80 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] 81 | 82 | [[licenses.clarify]] 83 | name = "ring" 84 | expression = "MIT AND ISC AND OpenSSL" 85 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 86 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | # If you see this, run "rustup self update" to get rustup 1.23 or newer. 2 | 3 | # NOTE: above comment is for older `rustup` (before TOML support was added), 4 | # which will treat the first line as the toolchain name, and therefore show it 5 | # to the user in the error, instead of "error: invalid channel name '[toolchain]'". 6 | 7 | [toolchain] 8 | channel = "1.65.0" 9 | components = [ "rustfmt", "clippy" ] 10 | targets = [ "wasm32-unknown-unknown" ] 11 | -------------------------------------------------------------------------------- /scripts/build_demo_web.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | ./scripts/setup_web.sh 7 | 8 | # This is required to enable the web_sys clipboard API which eframe web uses 9 | # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html 10 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 11 | export RUSTFLAGS=--cfg=web_sys_unstable_apis 12 | 13 | CRATE_NAME="egui_demo_app" 14 | 15 | # NOTE: persistence use up about 400kB (10%) of the WASM! 16 | FEATURES="http,persistence,web_screen_reader" 17 | 18 | OPEN=false 19 | OPTIMIZE=false 20 | BUILD=debug 21 | BUILD_FLAGS="" 22 | WEB_GPU=false 23 | 24 | while test $# -gt 0; do 25 | case "$1" in 26 | -h|--help) 27 | echo "build_demo_web.sh [--release] [--webgpu] [--open]" 28 | echo "" 29 | echo " --release: Build with --release, and enable extra optimization step" 30 | echo " Runs wasm-opt." 31 | echo " NOTE: --release also removes debug symbols which are otherwise useful for in-browser profiling." 32 | echo "" 33 | echo " --webgpu: Build a binary for WebGPU instead of WebGL" 34 | echo " Note that the resulting wasm will ONLY work on browsers with WebGPU." 35 | echo "" 36 | echo " --open: Open the result in a browser" 37 | exit 0 38 | ;; 39 | 40 | --release) 41 | shift 42 | OPTIMIZE=true 43 | BUILD="release" 44 | BUILD_FLAGS="--release" 45 | ;; 46 | 47 | --webgpu) 48 | shift 49 | WEB_GPU=true 50 | ;; 51 | 52 | --open) 53 | shift 54 | OPEN=true 55 | ;; 56 | 57 | *) 58 | break 59 | ;; 60 | esac 61 | done 62 | 63 | OUT_FILE_NAME="egui_demo_app" 64 | 65 | if [[ "${WEB_GPU}" == true ]]; then 66 | FEATURES="${FEATURES},wgpu" 67 | else 68 | FEATURES="${FEATURES},glow" 69 | fi 70 | 71 | FINAL_WASM_PATH=docs/${OUT_FILE_NAME}_bg.wasm 72 | 73 | # Clear output from old stuff: 74 | rm -f "${FINAL_WASM_PATH}" 75 | 76 | echo "Building rust…" 77 | 78 | (cd crates/$CRATE_NAME && 79 | cargo build \ 80 | ${BUILD_FLAGS} \ 81 | --lib \ 82 | --target wasm32-unknown-unknown \ 83 | --no-default-features \ 84 | --features ${FEATURES} 85 | ) 86 | 87 | # Get the output directory (in the workspace it is in another location) 88 | # TARGET=`cargo metadata --format-version=1 | jq --raw-output .target_directory` 89 | TARGET="target" 90 | 91 | echo "Generating JS bindings for wasm…" 92 | TARGET_NAME="${CRATE_NAME}.wasm" 93 | WASM_PATH="${TARGET}/wasm32-unknown-unknown/$BUILD/$TARGET_NAME" 94 | wasm-bindgen "${WASM_PATH}" --out-dir docs --out-name ${OUT_FILE_NAME} --no-modules --no-typescript 95 | 96 | # if this fails with "error: cannot import from modules (`env`) with `--no-modules`", you can use: 97 | # wasm2wat target/wasm32-unknown-unknown/release/egui_demo_app.wasm | rg env 98 | # wasm2wat target/wasm32-unknown-unknown/release/egui_demo_app.wasm | rg "call .now\b" -B 20 # What calls `$now` (often a culprit) 99 | 100 | # to get wasm-strip: apt/brew/dnf install wabt 101 | # wasm-strip ${FINAL_WASM_PATH} 102 | 103 | if [[ "${OPTIMIZE}" = true ]]; then 104 | echo "Optimizing wasm…" 105 | # to get wasm-opt: apt/brew/dnf install binaryen 106 | wasm-opt "${FINAL_WASM_PATH}" -O2 --fast-math -o "${FINAL_WASM_PATH}" # add -g to get debug symbols 107 | fi 108 | 109 | echo "Finished ${FINAL_WASM_PATH}" 110 | 111 | if [[ "${OPEN}" == true ]]; then 112 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 113 | # Linux, ex: Fedora 114 | xdg-open http://localhost:8888/index.html 115 | elif [[ "$OSTYPE" == "msys" ]]; then 116 | # Windows 117 | start http://localhost:8888/index.html 118 | else 119 | # Darwin/MacOS, or something else 120 | open http://localhost:8888/index.html 121 | fi 122 | fi 123 | -------------------------------------------------------------------------------- /scripts/cargo_deny.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 5 | cd "$script_path/.." 6 | set -x 7 | 8 | # cargo install cargo-deny 9 | cargo deny --all-features --log-level error --target aarch64-apple-darwin check 10 | cargo deny --all-features --log-level error --target i686-pc-windows-gnu check 11 | cargo deny --all-features --log-level error --target i686-pc-windows-msvc check 12 | cargo deny --all-features --log-level error --target i686-unknown-linux-gnu check 13 | cargo deny --all-features --log-level error --target wasm32-unknown-unknown check 14 | cargo deny --all-features --log-level error --target x86_64-apple-darwin check 15 | cargo deny --all-features --log-level error --target x86_64-pc-windows-gnu check 16 | cargo deny --all-features --log-level error --target x86_64-pc-windows-msvc check 17 | cargo deny --all-features --log-level error --target x86_64-unknown-linux-gnu check 18 | cargo deny --all-features --log-level error --target x86_64-unknown-linux-musl check 19 | cargo deny --all-features --log-level error --target x86_64-unknown-redox check 20 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This scripts runs various CI-like checks in a convenient way. 3 | 4 | set -eu 5 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 6 | cd "$script_path/.." 7 | set -x 8 | 9 | # Checks all tests, lints etc. 10 | # Basically does what the CI does. 11 | 12 | cargo install cargo-cranky # Uses lints defined in Cranky.toml. See https://github.com/ericseppanen/cargo-cranky 13 | 14 | # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, 15 | # as well as by the wasm32-backend of the wgpu crate. 16 | export RUSTFLAGS="--cfg=web_sys_unstable_apis -D warnings" 17 | export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 18 | 19 | cargo check --all-targets 20 | cargo check --all-targets --all-features 21 | cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown 22 | cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features 23 | cargo cranky --all-targets --all-features -- -D warnings 24 | cargo test --all-targets --all-features 25 | cargo test --doc # slow - checks all doc-tests 26 | cargo fmt --all -- --check 27 | 28 | cargo doc --lib --no-deps --all-features 29 | cargo doc --document-private-items --no-deps --all-features 30 | 31 | (cd crates/eframe && cargo check --no-default-features --features "glow") 32 | (cd crates/eframe && cargo check --no-default-features --features "wgpu") 33 | (cd crates/egui && cargo check --no-default-features --features "serde") 34 | (cd crates/egui_demo_app && cargo check --no-default-features --features "glow") 35 | (cd crates/egui_demo_app && cargo check --no-default-features --features "wgpu") 36 | (cd crates/egui_demo_lib && cargo check --no-default-features) 37 | (cd crates/egui_extras && cargo check --no-default-features) 38 | (cd crates/egui_glow && cargo check --no-default-features) 39 | (cd crates/egui-winit && cargo check --no-default-features --features "wayland") 40 | (cd crates/egui-winit && cargo check --no-default-features --features "winit/x11") 41 | (cd crates/emath && cargo check --no-default-features) 42 | (cd crates/epaint && cargo check --no-default-features --release) 43 | (cd crates/epaint && cargo check --no-default-features) 44 | 45 | (cd crates/eframe && cargo check --all-features) 46 | (cd crates/egui && cargo check --all-features) 47 | (cd crates/egui_demo_app && cargo check --all-features) 48 | (cd crates/egui_extras && cargo check --all-features) 49 | (cd crates/egui_glow && cargo check --all-features) 50 | (cd crates/egui-winit && cargo check --all-features) 51 | (cd crates/emath && cargo check --all-features) 52 | (cd crates/epaint && cargo check --all-features) 53 | 54 | ./scripts/wasm_bindgen_check.sh 55 | 56 | cargo cranky --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings 57 | 58 | ./scripts/cargo_deny.sh 59 | 60 | # TODO(emilk): consider using https://github.com/taiki-e/cargo-hack or https://github.com/frewsxcv/cargo-all-features 61 | 62 | # ------------------------------------------------------------ 63 | # 64 | 65 | # For finding bloat: 66 | # cargo bloat --release --bin egui_demo_app -n 200 | rg egui 67 | # Also try https://github.com/google/bloaty 68 | 69 | # what compiles slowly? 70 | # cargo clean && time cargo build -p eframe --timings 71 | # https://fasterthanli.me/articles/why-is-my-rust-build-so-slow 72 | 73 | # what compiles slowly? 74 | # cargo llvm-lines --lib -p egui | head -20 75 | 76 | echo "All checks passed." 77 | -------------------------------------------------------------------------------- /scripts/clippy_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This scripts run clippy on the wasm32-unknown-unknown target, 3 | # using a special clippy.toml config file which forbids a few more things. 4 | 5 | set -eu 6 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 7 | cd "$script_path/.." 8 | set -x 9 | 10 | # Use scripts/clippy_wasm/clippy.toml 11 | export CLIPPY_CONF_DIR="scripts/clippy_wasm" 12 | 13 | cargo cranky --all-features --target wasm32-unknown-unknown --target-dir target_wasm -p egui_demo_app --lib -- --deny warnings 14 | -------------------------------------------------------------------------------- /scripts/clippy_wasm/clippy.toml: -------------------------------------------------------------------------------- 1 | # This is used by `scripts/clippy_wasm.sh` so we can forbid some methods that are not available in wasm. 2 | # 3 | # We cannot forbid all these methods in the main `clippy.toml` because of 4 | # https://github.com/rust-lang/rust-clippy/issues/10406 5 | 6 | msrv = "1.65" 7 | 8 | # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods 9 | disallowed-methods = [ 10 | "std::time::Instant::now", # use `instant` crate instead for wasm/web compatibility 11 | "std::time::Duration::elapsed", # use `instant` crate instead for wasm/web compatibility 12 | "std::time::SystemTime::now", # use `instant` or `time` crates instead for wasm/web compatibility 13 | 14 | # Cannot spawn threads on wasm: 15 | "std::thread::spawn", 16 | ] 17 | 18 | # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types 19 | disallowed-types = [ 20 | # Cannot spawn threads on wasm: 21 | "std::thread::Builder", 22 | ] 23 | 24 | # Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown 25 | doc-valid-idents = [ 26 | # You must also update the same list in the root `clippy.toml`! 27 | "AccessKit", 28 | "..", 29 | ] 30 | -------------------------------------------------------------------------------- /scripts/docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | cargo doc -p eframe --target wasm32-unknown-unknown --lib --no-deps 7 | cargo doc -p emath -p epaint -p egui -p eframe -p egui-winit -p egui_extras -p egui_glow --lib --no-deps --all-features --open 8 | 9 | # cargo watch -c -x 'doc -p emath -p epaint -p egui --lib --no-deps --all-features' 10 | -------------------------------------------------------------------------------- /scripts/find_bloat.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | cargo bloat --release --bin egui_demo_app -n 200 | rg "egui " 7 | 8 | cargo llvm-lines -p egui_demo_lib | rg egui | head -30 9 | -------------------------------------------------------------------------------- /scripts/generate_changelog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Summarizes recent PRs based on their GitHub labels. 5 | 6 | The result can be copy-pasted into CHANGELOG.md, though it often needs some manual editing too. 7 | """ 8 | 9 | import multiprocessing 10 | import re 11 | import sys 12 | from dataclasses import dataclass 13 | from typing import Any, List, Optional 14 | 15 | import requests 16 | from git import Repo # pip install GitPython 17 | from tqdm import tqdm 18 | 19 | OWNER = "emilk" 20 | REPO = "egui" 21 | COMMIT_RANGE = "latest..HEAD" 22 | INCLUDE_LABELS = False # It adds quite a bit of visual noise 23 | OFFICIAL_DEVS = [ 24 | "emilk", 25 | ] 26 | 27 | 28 | @dataclass 29 | class PrInfo: 30 | gh_user_name: str 31 | pr_title: str 32 | labels: List[str] 33 | 34 | 35 | @dataclass 36 | class CommitInfo: 37 | hexsha: str 38 | title: str 39 | pr_number: Optional[int] 40 | 41 | 42 | def get_github_token() -> str: 43 | import os 44 | 45 | token = os.environ.get("GH_ACCESS_TOKEN", "") 46 | if token != "": 47 | return token 48 | 49 | home_dir = os.path.expanduser("~") 50 | token_file = os.path.join(home_dir, ".githubtoken") 51 | 52 | try: 53 | with open(token_file, "r") as f: 54 | token = f.read().strip() 55 | return token 56 | except Exception: 57 | pass 58 | 59 | print("ERROR: expected a GitHub token in the environment variable GH_ACCESS_TOKEN or in ~/.githubtoken") 60 | sys.exit(1) 61 | 62 | 63 | # Slow 64 | def fetch_pr_info_from_commit_info(commit_info: CommitInfo) -> Optional[PrInfo]: 65 | if commit_info.pr_number is None: 66 | return None 67 | else: 68 | return fetch_pr_info(commit_info.pr_number) 69 | 70 | 71 | # Slow 72 | def fetch_pr_info(pr_number: int) -> Optional[PrInfo]: 73 | url = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}" 74 | gh_access_token = get_github_token() 75 | headers = {"Authorization": f"Token {gh_access_token}"} 76 | response = requests.get(url, headers=headers) 77 | json = response.json() 78 | 79 | # Check if the request was successful (status code 200) 80 | if response.status_code == 200: 81 | labels = [label["name"] for label in json["labels"]] 82 | gh_user_name = json["user"]["login"] 83 | return PrInfo(gh_user_name=gh_user_name, pr_title=json["title"], labels=labels) 84 | else: 85 | print(f"ERROR {url}: {response.status_code} - {json['message']}") 86 | return None 87 | 88 | 89 | def get_commit_info(commit: Any) -> CommitInfo: 90 | match = re.match(r"(.*) \(#(\d+)\)", commit.summary) 91 | if match: 92 | return CommitInfo(hexsha=commit.hexsha, title=str(match.group(1)), pr_number=int(match.group(2))) 93 | else: 94 | return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None) 95 | 96 | 97 | def remove_prefix(text, prefix): 98 | if text.startswith(prefix): 99 | return text[len(prefix):] 100 | return text # or whatever 101 | 102 | 103 | def print_section(crate: str, items: List[str]) -> None: 104 | if 0 < len(items): 105 | print(f"#### {crate}") 106 | for line in items: 107 | line = remove_prefix(line, f"{crate}: ") 108 | line = remove_prefix(line, f"[{crate}] ") 109 | print(f"* {line}") 110 | print() 111 | 112 | 113 | def main() -> None: 114 | repo = Repo(".") 115 | commits = list(repo.iter_commits(COMMIT_RANGE)) 116 | commits.reverse() # Most recent last 117 | commit_infos = list(map(get_commit_info, commits)) 118 | 119 | pool = multiprocessing.Pool() 120 | pr_infos = list( 121 | tqdm( 122 | pool.imap(fetch_pr_info_from_commit_info, commit_infos), 123 | total=len(commit_infos), 124 | desc="Fetch PR info commits", 125 | ) 126 | ) 127 | 128 | ignore_labels = ["CI", "dependencies"] 129 | 130 | crate_names = [ 131 | "ecolor", 132 | "eframe", 133 | "egui_extras", 134 | "egui_glow", 135 | "egui-wgpu", 136 | "egui-winit", 137 | "egui", 138 | "epaint", 139 | ] 140 | sections = {} 141 | unsorted_prs = [] 142 | unsorted_commits = [] 143 | 144 | for commit_info, pr_info in zip(commit_infos, pr_infos): 145 | hexsha = commit_info.hexsha 146 | title = commit_info.title 147 | pr_number = commit_info.pr_number 148 | 149 | if pr_number is None: 150 | # Someone committed straight to main: 151 | summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})" 152 | unsorted_commits.append(summary) 153 | else: 154 | title = pr_info.pr_title if pr_info else title # We prefer the PR title if available 155 | labels = pr_info.labels if pr_info else [] 156 | 157 | summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})" 158 | 159 | if INCLUDE_LABELS and 0 < len(labels): 160 | summary += f" ({', '.join(labels)})" 161 | 162 | if pr_info is not None: 163 | gh_user_name = pr_info.gh_user_name 164 | if gh_user_name not in OFFICIAL_DEVS: 165 | summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)" 166 | 167 | added = False 168 | for crate in crate_names: 169 | if crate in labels: 170 | sections.setdefault(crate, []).append(summary) 171 | added = True 172 | 173 | if not added: 174 | if not any(label in labels for label in ignore_labels): 175 | unsorted_prs.append(summary) 176 | 177 | print() 178 | for crate in crate_names: 179 | if crate in sections: 180 | summary = sections[crate] 181 | print_section(crate, summary) 182 | print_section("Unsorted PRs", unsorted_prs) 183 | print_section("Unsorted commits", unsorted_commits) 184 | 185 | 186 | if __name__ == "__main__": 187 | main() 188 | -------------------------------------------------------------------------------- /scripts/generate_example_screenshots.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script generates screenshots for all the examples in examples/ 3 | 4 | set -eu 5 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 6 | cd "$script_path/.." 7 | 8 | cd examples 9 | for EXAMPLE_NAME in $(ls -1d */ | sed 's/\/$//'); do 10 | if [ ${EXAMPLE_NAME} != "hello_world_par" ] && [ ${EXAMPLE_NAME} != "screenshot" ]; then 11 | echo "" 12 | echo "Running ${EXAMPLE_NAME}…" 13 | EFRAME_SCREENSHOT_TO="temp.png" cargo run -p ${EXAMPLE_NAME} 14 | pngcrush -rem allb -brute -reduce temp.png "${EXAMPLE_NAME}/screenshot.png" 15 | rm temp.png 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /scripts/setup_web.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | # Pre-requisites: 7 | rustup target add wasm32-unknown-unknown 8 | 9 | # For generating JS bindings: 10 | cargo install wasm-bindgen-cli --version 0.2.86 11 | -------------------------------------------------------------------------------- /scripts/start_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | # Starts a local web-server that serves the contents of the `doc/` folder, 7 | # i.e. the web-version of `egui_demo_app`. 8 | 9 | PORT=8888 10 | 11 | echo "ensuring basic-http-server is installed…" 12 | cargo install basic-http-server 13 | 14 | echo "starting server…" 15 | echo "serving at http://localhost:${PORT}" 16 | 17 | (cd docs && basic-http-server --addr 0.0.0.0:${PORT} .) 18 | # (cd docs && python3 -m http.server ${PORT} --bind 0.0.0.0) 19 | -------------------------------------------------------------------------------- /scripts/wasm_bindgen_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path/.." 5 | 6 | if [[ $* == --skip-setup ]] 7 | then 8 | echo "Skipping setup_web.sh" 9 | else 10 | echo "Running setup_web.sh" 11 | ./scripts/setup_web.sh 12 | fi 13 | 14 | CRATE_NAME="egui_demo_app" 15 | FEATURES="glow,http,persistence,web_screen_reader" 16 | 17 | # This is required to enable the web_sys clipboard API which eframe web uses 18 | # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html 19 | # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html 20 | export RUSTFLAGS=--cfg=web_sys_unstable_apis 21 | 22 | echo "Building rust…" 23 | BUILD=debug # debug builds are faster 24 | 25 | (cd crates/$CRATE_NAME && 26 | cargo build \ 27 | --lib \ 28 | --target wasm32-unknown-unknown \ 29 | --no-default-features \ 30 | --features ${FEATURES} 31 | ) 32 | 33 | TARGET="target" 34 | 35 | echo "Generating JS bindings for wasm…" 36 | 37 | rm -f "${CRATE_NAME}_bg.wasm" # Remove old output (if any) 38 | 39 | TARGET_NAME="${CRATE_NAME}.wasm" 40 | wasm-bindgen "${TARGET}/wasm32-unknown-unknown/$BUILD/$TARGET_NAME" \ 41 | --out-dir . --no-modules --no-typescript 42 | 43 | # Remove output: 44 | rm -f "${CRATE_NAME}_bg.wasm" 45 | rm -f "${CRATE_NAME}.js" 46 | -------------------------------------------------------------------------------- /scripts/wasm_size.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 4 | cd "$script_path" 5 | 6 | ./build_demo_web.sh && ls -lh ../docs/*.wasm 7 | --------------------------------------------------------------------------------