Compare commits

..

1 commit

Author SHA1 Message Date
Emil Ernerfeldt
df937af2c5 experiment a bit with overtone visualization 2021-02-20 16:50:53 +01:00
474 changed files with 31248 additions and 75217 deletions

View file

@ -1,6 +0,0 @@
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]

View file

@ -7,44 +7,32 @@ assignees: ''
--- ---
<!--
First look if there is already a similar bug report. If there is, upvote the issue with 👍
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:
[patch.crates-io]
egui = { git = "https://github.com/emilk/egui", branch = "master" }
# if you're using eframe:
eframe = { git = "https://github.com/emilk/egui", branch = "master" }
-->
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. An image is good, a gif or movie is better! --> A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. <!-- Go to '…' --> 1. Go to '...'
2. <!-- Click on '…' --> 2. Click on '....'
3. <!-- Scroll down to '…' --> 3. Scroll down to '....'
4. <!-- See error --> 4. See error
**Expected behavior** **Expected behavior**
<!-- A clear and concise description of what you expected to happen. --> A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
<!-- If applicable, add screenshots to help explain your problem. --> If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: <!-- e.g. iOS --> - OS: [e.g. iOS]
- Browser <!-- e.g. chrome, safari --> - Browser [e.g. chrome, safari]
- Version <!-- e.g. 22 --> - Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: <!-- e.g. iPhone6 --> - Device: [e.g. iPhone6]
- OS: <!-- e.g. iOS8.1 --> - OS: [e.g. iOS8.1]
- Browser <!-- e.g. stock browser, safari --> - Browser [e.g. stock browser, safari]
- Version <!-- e.g. 22 --> - Version [e.g. 22]
**Additional context** **Additional context**
<!-- Add any other context about the problem here. --> Add any other context about the problem here.

View file

@ -2,24 +2,19 @@
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: ''
labels: feature-request labels: enhancement
assignees: '' assignees: ''
--- ---
<!--
First look if there is already a similar feature request. If there is, upvote the issue with 👍
-->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when […] --> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** **Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. --> A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. --> A clear and concise description of any alternative solutions or features you've considered.
**Additional context** **Additional context**
<!-- Add any other context or screenshots about the feature request here. --> Add any other context or screenshots about the feature request here.

View file

@ -1,15 +0,0 @@
<!--
Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request!
* Keep your PR:s small and focused.
* If applicable, add a screenshot or gif.
* Unless this is a trivial change, add a line to the relevant `CHANGELOG.md` under "Unreleased".
* If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run `./sh/check.sh`.
* When you have addressed a PR comment, mark it as resolved.
Please be patient! I will review you PR, but my time is limited!
-->
Closes <https://github.com/emilk/egui/issues/THE_RELEVANT_ISSUE>.

View file

@ -3,209 +3,117 @@ on: [push, pull_request]
name: CI name: CI
env: env:
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, # This is required to enable the web_sys clipboard API which egui_web uses
# as well as by the wasm32-backend of the wgpu crate.
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings RUSTFLAGS: --cfg=web_sys_unstable_apis
RUSTDOCFLAGS: -D warnings
jobs: jobs:
fmt-crank-check-test: check_default:
name: Format + check + test name: cargo check (default features)
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: 1.65.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
#uses: awalsh128/cache-apt-pkgs-action@v1.2.2
#TODO(emilk) use upstream when https://github.com/awalsh128/cache-apt-pkgs-action/pull/90 is merged
uses: rerun-io/cache-apt-pkgs-action@59534850182063abf1b2c11bb3686722a12a8397
with:
packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
version: 1.0
execute_install_scripts: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Install cargo-cranky
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-cranky
- name: check --all-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --all-features --all-targets
- name: check default features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --all-targets
- name: check --no-default-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --no-default-features --lib --all-targets
- name: check eframe --no-default-features
uses: actions-rs/cargo@v1
with:
command: check
args: --locked --no-default-features --lib --all-targets -p eframe
- name: Test doc-tests
uses: actions-rs/cargo@v1
with:
command: test
args: --doc --all-features
- name: cargo doc --lib
uses: actions-rs/cargo@v1
with:
command: doc
args: --lib --no-deps --all-features
- name: cargo doc --document-private-items
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps --all-features
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features
- name: Cranky
uses: actions-rs/cargo@v1
with:
command: cranky
args: --all-targets --all-features -- -D warnings
# ---------------------------------------------------------------------------
check_wasm:
name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: 1.65.0 toolchain: stable
target: wasm32-unknown-unknown
override: true override: true
- uses: actions-rs/cargo@v1
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Install cargo-cranky
uses: baptiste0928/cargo-install@v1
with: with:
crate: cargo-cranky command: check
- name: Check wasm32 egui_demo_app check_all_features:
uses: actions-rs/cargo@v1 name: cargo check --all-features
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
check_web_default:
name: cargo check web (default features)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup target add wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with: with:
command: check command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown args: -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: Check wasm32 egui_demo_app --all-features check_web_all_features:
uses: actions-rs/cargo@v1 name: cargo check web --all-features
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup target add wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with: with:
command: check command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: Check wasm32 eframe test:
uses: actions-rs/cargo@v1 name: cargo test
with: runs-on: ubuntu-latest
command: check
args: -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.84"
- run: ./sh/wasm_bindgen_check.sh --skip-setup
- name: Cranky wasm32
uses: actions-rs/cargo@v1
with:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
# ---------------------------------------------------------------------------
cargo-deny:
name: cargo deny
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
rust-version: "1.65.0"
# ---------------------------------------------------------------------------
android:
name: android
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
target: aarch64-linux-android
override: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- run: cargo check --features wgpu --target aarch64-linux-android
working-directory: crates/eframe
# ---------------------------------------------------------------------------
windows:
name: Check Windows
runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: 1.65.0 toolchain: stable
override: true override: true
- run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- name: Set up cargo cache - uses: actions-rs/cargo@v1
uses: Swatinem/rust-cache@v2
- name: Check
uses: actions-rs/cargo@v1
with: with:
command: check command: test
args: --all-targets --all-features args: --all-features
fmt:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: cargo clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings

3
.gitignore vendored
View file

@ -1,7 +1,4 @@
.DS_Store
**/target **/target
**/target_ra
**/target_wasm
/.*.json /.*.json
/.vscode /.vscode
/media/* /media/*

32
.vscode/settings.json vendored
View file

@ -1,32 +0,0 @@
{
"files.insertFinalNewline": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"editor.semanticTokenColorCustomizations": {
"rules": {
"*.unsafe:rust": "#eb5046"
}
},
"files.exclude": {
"target/**": true,
"target_ra/**": true,
},
// 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`
"rust-analyzer.checkOnSave.overrideCommand": [
"cargo",
"cranky",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets"
],
"rust-analyzer.cargo.buildScripts.overrideCommand": [
"cargo",
"check",
"--quiet",
"--target-dir=target_ra",
"--workspace",
"--message-format=json",
"--all-targets"
],
}

View file

@ -1,53 +0,0 @@
# Architecture
This document describes how the crates that make up egui are all connected.
Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) for what to do before opening a PR.
## Crate overview
The crates in this repository are: `egui, emath, epaint, egui_extras, egui-winit, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`.
### `egui`: The main GUI library.
Example code: `if ui.button("Click me").clicked() { … }`
This is the crate where the bulk of the code is at. `egui` depends only on `emath` and `epaint`.
### `emath`: minimal 2D math library
Examples: `Vec2, Pos2, Rect, lerp, remap`
### `epaint`
2d shapes and text that can be turned into textured triangles.
Example: `Shape::Circle { center, radius, fill, stroke }`
Depends on `emath`.
### `egui_extras`
This adds additional features on top of `egui`.
### `egui-winit`
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit).
The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc.
### `egui_glium`
Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glium](https://github.com/glium/glium).
### `egui_glow`
Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glow](https://github.com/grovesNL/glow).
### `eframe`
`eframe` is the official `egui` framework, built so you can compile the same app for either web or native.
The demo that you can see at <https://www.egui.rs> is using `eframe` to host the `egui`. The demo code is found in:
### `egui_demo_lib`
Depends on `egui`.
This contains a bunch of uses of `egui` and looks like the ui code you would write for an `egui` app.
### `egui_demo_app`
Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a native app executable.
Depends on `egui_demo_lib` + `eframe`.
### Other integrations
There are also many great integrations for game engines such as `bevy` and `miniquad` which you can find at <https://github.com/emilk/egui#integrations>.

View file

@ -1,623 +1,24 @@
# egui changelog # egui changelog
All notable changes to the `egui` crate will be documented in this file.
NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! All notable changes to the egui crate will be documented in this file.
NOTE: `epi`, `eframe`, `egui_web` and `egui_glium` has their own changelogs!
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
### Added ⭐ ### Added ⭐
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)). * Add `egui::plot::Plot` to plot some 2D data.
* `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Ui::hyperlink_to(label, url)`.
* Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider.
* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)).
* Add `ScrollArea::drag_to_scroll` if you want to turn off that feature.
* Add `Response::on_hover_and_drag_cursor`.
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
### Changed 🔧 ### Changed 🔧
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
* Improved the algorithm for picking the number of decimals to show when hovering values in the `Plot`.
* Default `ComboBox` is now controlled with `Spacing::combo_width` ([#2621](https://github.com/emilk/egui/pull/2621)).
* `DragValue` and `Slider` now use the proportional font ([#2638](https://github.com/emilk/egui/pull/2638)).
* `ScrollArea` is less aggressive about clipping its contents ([#2665](https://github.com/emilk/egui/pull/2665)).
* Updated to be compatible with a major breaking change in AccessKit that drastically reduces memory usage when accessibility is enabled ([#2678](https://github.com/emilk/egui/pull/2678)).
* Improve `DragValue` behavior ([#2649](https://github.com/emilk/egui/pull/2649), [#2650](https://github.com/emilk/egui/pull/2650), [#2688](https://github.com/emilk/egui/pull/2688), [#2638](https://github.com/emilk/egui/pull/2638)).
### Fixed 🐛
* Trigger `PointerEvent::Released` for drags ([#2507](https://github.com/emilk/egui/pull/2507)).
* Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)).
* Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)).
* The `button_padding` style option works closer as expected with image+text buttons now ([#2510](https://github.com/emilk/egui/pull/2510)).
* Menus are now moved to fit on the screen.
* Fix `Window::pivot` causing windows to move around ([#2694](https://github.com/emilk/egui/pull/2694)).
## 0.20.1 - 2022-12-11 - Fix key-repeat
### Changed 🔧
* `InputState`: all press functions again include key repeats (like in egui 0.19) ([#2429](https://github.com/emilk/egui/pull/2429)).
* Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)).
### Fixed 🐛
* Fix key-repeats for `TextEdit`, `Slider`s, etc ([#2429](https://github.com/emilk/egui/pull/2429)).
## 0.20.0 - 2022-12-08 - AccessKit, prettier text, overlapping widgets
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)).
### Added ⭐
* Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)).
* Added `Context::os/Context::set_os` to query/set what operating system egui believes it is running on ([#2202](https://github.com/emilk/egui/pull/2202)).
* Added `Button::shortcut_text` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
* Added `egui::KeyboardShortcut` for showing keyboard shortcuts in menu buttons ([#2202](https://github.com/emilk/egui/pull/2202)).
* Texture loading now takes a `TexureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)).
* Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)).
* Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)).
* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)).
* Added `spacing.menu_margin` for customizing menu spacing ([#2036](https://github.com/emilk/egui/pull/2036))
* Added possibility to enable text wrap for the selected text of `egui::ComboBox` ([#2272](https://github.com/emilk/egui/pull/2272))
* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds ([#2270](https://github.com/emilk/egui/pull/2270)).
* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position ([#2303](https://github.com/emilk/egui/pull/2303)).
* Added support for [thin space](https://en.wikipedia.org/wiki/Thin_space).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
* Added `panel_fill`, `window_fill` and `window_stroke` to `Visuals` for your theming pleasure ([#2406](https://github.com/emilk/egui/pull/2406)).
* Plots:
* Allow linking plot cursors ([#1722](https://github.com/emilk/egui/pull/1722)).
* Added `Plot::auto_bounds_x/y` and `Plot::reset` ([#2029](https://github.com/emilk/egui/pull/2029)).
* Added `PlotUi::translate_bounds` ([#2145](https://github.com/emilk/egui/pull/2145)).
* Added `PlotUi::set_plot_bounds` ([#2320](https://github.com/emilk/egui/pull/2320)).
* Added `PlotUi::plot_secondary_clicked` ([#2318](https://github.com/emilk/egui/pull/2318)).
### Changed 🔧
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
* Make it slightly easier to click buttons ([#2304](https://github.com/emilk/egui/pull/2304)).
* `egui::color` has been renamed `egui::ecolor` ([#2399](https://github.com/emilk/egui/pull/2399)).
### Fixed 🐛
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
* Improve mixed CJK/Latin line-breaking ([#1986](https://github.com/emilk/egui/pull/1986)).
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
* Constrain menu popups to the screen ([#2191](https://github.com/emilk/egui/pull/2191)).
* Less jitter when calling `Context::set_pixels_per_point` ([#2239](https://github.com/emilk/egui/pull/2239)).
* Fixed popups and color edit going outside the screen.
* Fixed keyboard support in `DragValue` ([#2342](https://github.com/emilk/egui/pull/2342)).
* If you nest `ScrollAreas` inside each other, the inner area will now move its scroll bar so it is always visible ([#2371](https://github.com/emilk/egui/pull/2371)).
* Ignore key-repeats for `input.key_pressed` ([#2334](https://github.com/emilk/egui/pull/2334), [#2389](https://github.com/emilk/egui/pull/2389)).
* Fixed issue with calling `set_pixels_per_point` each frame ([#2352](https://github.com/emilk/egui/pull/2352)).
* Fix bug in `ScrollArea::show_rows` ([#2258](https://github.com/emilk/egui/pull/2258)).
* Fix bug in `plot::Line::fill` ([#2275](https://github.com/emilk/egui/pull/2275)).
* Only emit `changed` events in `radio_value` and `selectable_value` if the value actually changed ([#2343](https://github.com/emilk/egui/pull/2343)).
* Fixed sizing bug in `Grid` ([#2384](https://github.com/emilk/egui/pull/2384)).
* `ComboBox::width` now correctly sets the outer width ([#2406](https://github.com/emilk/egui/pull/2406)).
## 0.19.0 - 2022-08-20
### Added ⭐
* Added `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)).
* Added `PointerButton::Extra1` and `PointerButton::Extra2` ([#1592](https://github.com/emilk/egui/pull/1592)).
* Added `egui::hex_color!` to create `Color32`'s from hex strings under the `color-hex` feature ([#1596](https://github.com/emilk/egui/pull/1596)).
* Optimized painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).
* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)).
* Added `InputState::stable_dt`: a more stable estimate for the delta-time in reactive mode ([#1625](https://github.com/emilk/egui/pull/1625)).
* You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)).
* Added functions keys in `egui::Key` ([#1665](https://github.com/emilk/egui/pull/1665)).
* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)).
* Added `Context::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
* Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)).
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
* Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)).
* Added `custom_formatter`, `binary`, `octal`, and `hexadecimal` to `DragValue` and `Slider` ([#1953](https://github.com/emilk/egui/issues/1953))
### Changed 🔧
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* `PaintCallback` shapes now require the whole callback to be put in an `Arc<dyn Any>` with the value being a backend-specific callback type ([#1684](https://github.com/emilk/egui/pull/1684)).
* Replaced `needs_repaint` in `FullOutput` with `repaint_after`. Used to force repaint after the set duration in reactive mode ([#1694](https://github.com/emilk/egui/pull/1694)).
* `Layout::left_to_right` and `Layout::right_to_left` now takes the vertical align as an argument. Previous default was `Align::Center`.
* Improved ergonomics of adding plot items. All plot items that take a series of 2D coordinates can now be created directly from `Vec<[f64; 2]>`. The `Value` and `Values` types were removed in favor of `PlotPoint` and `PlotPoints` respectively ([#1816](https://github.com/emilk/egui/pull/1816)).
* `TextBuffer` no longer needs to implement `AsRef<str>` ([#1824](https://github.com/emilk/egui/pull/1824)).
### Fixed 🐛
* Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)).
* Fixed `ImageButton`'s changing background padding on hover ([#1595](https://github.com/emilk/egui/pull/1595)).
* Fixed `Plot` auto-bounds bug ([#1599](https://github.com/emilk/egui/pull/1599)).
* Fixed dead-lock when alt-tabbing while also showing a tooltip ([#1618](https://github.com/emilk/egui/pull/1618)).
* Fixed `ScrollArea` scrolling when editing an unrelated `TextEdit` ([#1779](https://github.com/emilk/egui/pull/1779)).
* Fixed `Slider` not always generating events on change ([#1854](https://github.com/emilk/egui/pull/1854)).
* Fixed jitter of anchored windows for the first frame ([#1856](https://github.com/emilk/egui/pull/1856)).
* Fixed focus behavior when pressing Tab in a UI with no focused widget ([#1861](https://github.com/emilk/egui/pull/1861)).
* Fixed automatic plot bounds ([#1865](https://github.com/emilk/egui/pull/1865)).
## 0.18.1 - 2022-05-01
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
## 0.18.0 - 2022-04-30
### Added ⭐
* Added `Shape::Callback` for backend-specific painting, [with an example](https://github.com/emilk/egui/tree/master/examples/custom_3d_glow) ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Frame::canvas` ([#1362](https://github.com/emilk/egui/pull/1362)).
* `Context::request_repaint` will now wake up UI thread, if integrations has called `Context::set_request_repaint_callback` ([#1366](https://github.com/emilk/egui/pull/1366)).
* Added `Plot::allow_scroll`, `Plot::allow_zoom` no longer affects scrolling ([#1382](https://github.com/emilk/egui/pull/1382)).
* Added `Ui::push_id` to resolve id clashes ([#1374](https://github.com/emilk/egui/pull/1374)).
* Added `ComboBox::icon` ([#1405](https://github.com/emilk/egui/pull/1405)).
* Added `Ui::scroll_with_delta`.
* Added `Frame::outer_margin`.
* Added `Painter::hline` and `Painter::vline`.
* Added `Link` and `ui.link` ([#1506](https://github.com/emilk/egui/pull/1506)).
* Added triple-click support; triple-clicking a TextEdit field will select the whole paragraph ([#1512](https://github.com/emilk/egui/pull/1512)).
* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)).
* Added `Ui::spinner()` shortcut method ([#1494](https://github.com/emilk/egui/pull/1494)).
* Added `CursorIcon`s for resizing columns, rows, and the eight cardinal directions.
* Added `Ui::toggle_value`.
* Added ability to add any widgets to the header of a collapsing region ([#1538](https://github.com/emilk/egui/pull/1538)).
### Changed 🔧
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).
* Renamed `Frame::margin` to `Frame::inner_margin`.
* Renamed `AlphaImage` to `FontImage` to discourage any other use for it ([#1412](https://github.com/emilk/egui/pull/1412)).
* Warnings will be painted on screen when there is an `Id` clash for `Grid`, `Plot` or `ScrollArea` ([#1452](https://github.com/emilk/egui/pull/1452)).
* `Checkbox` and `RadioButton` with an empty label (`""`) will now take up much less space ([#1456](https://github.com/emilk/egui/pull/1456)).
* Replaced `Memory::top_most_layer` with more flexible `Memory::layer_ids`.
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed `Painter::sub_region` to `Painter::with_clip_rect`.
### Fixed 🐛
* Fixed `ComboBox`es always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)).
* Fixed ui code that could lead to a deadlock ([#1380](https://github.com/emilk/egui/pull/1380)).
* Text is darker and more readable in bright mode ([#1412](https://github.com/emilk/egui/pull/1412)).
* Fixed a lot of broken/missing doclinks ([#1419](https://github.com/emilk/egui/pull/1419)).
* Fixed `Ui::add_visible` sometimes leaving the `Ui` in a disabled state ([#1436](https://github.com/emilk/egui/issues/1436)).
* Added line breaking rules for Japanese text ([#1498](https://github.com/emilk/egui/pull/1498)).
### Deprecated ☢️
* Deprecated `CollapsingHeader::selectable` ([#1538](https://github.com/emilk/egui/pull/1538)).
### Removed 🔥
* Removed the `single_threaded/multi_threaded` flags - egui is now always thread-safe ([#1390](https://github.com/emilk/egui/pull/1390)).
### Contributors 🙏
* [4JX](https://github.com/4JX)
* [AlexxxRu](https://github.com/AlexxxRu)
* [ascclemens](https://github.com/ascclemens)
* [awaken1ng](https://github.com/awaken1ng)
* [bigfarts](https://github.com/bigfarts)
* [bobyclaws](https://github.com/bobyclaws)
* [Bromeon](https://github.com/Bromeon)
* [cloudhead](https://github.com/cloudhead)
* [collin-kemper](https://github.com/collin-kemper)
* [cpterry](https://github.com/cpterry)
* [dbuch](https://github.com/dbuch)
* [DusterTheFirst](https://github.com/DusterTheFirst)
* [Edgeworth ](https://github.com/Edgeworth )
* [elwerene](https://github.com/elwerene)
* [follower](https://github.com/follower)
* [Friz64](https://github.com/Friz64)
* [Hunter522 ](https://github.com/Hunter522 )
* [Jake-Shadle](https://github.com/Jake-Shadle)
* [jean-airoldie ](https://github.com/jean-airoldie )
* [JelNiSlaw](https://github.com/JelNiSlaw)
* [juancampa](https://github.com/juancampa)
* [LU15W1R7H](https://github.com/LU15W1R7H)
* [mbillingr](https://github.com/mbillingr)
* [nicklasmoeller](https://github.com/nicklasmoeller)
* [rukai](https://github.com/rukai)
* [tami5](https://github.com/tami5)
* [Titaniumtown](https://github.com/Titaniumtown)
* [trevyn](https://github.com/trevyn)
* [waynr](https://github.com/waynr)
* [zam-5 ](https://github.com/zam-5 )
## 0.17.0 - 2022-02-22 - Improved font selection and image handling
### Added ⭐
* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)):
* You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`.
* Easily change text styles with `Style::text_styles`.
* Added `Ui::text_style_height`.
* Added `TextStyle::resolve`.
* Made the v-align and scale of user fonts tweakable ([#1241](https://github.com/emilk/egui/pull/1027)).
* Plot:
* Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)).
* Added `Plot::allow_boxed_zoom()`, `Plot::boxed_zoom_pointer()` for boxed zooming on plots ([#1188](https://github.com/emilk/egui/pull/1188)).
* Added plot pointer coordinates with `Plot::coordinates_formatter` ([#1235](https://github.com/emilk/egui/pull/1235)).
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
* `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)).
* `Ui::input_mut` to modify how subsequent widgets see the `InputState` and a convenience method `InputState::consume_key` for shortcuts or hotkeys ([#1212](https://github.com/emilk/egui/pull/1212)).
* Added `Ui::add_visible` and `Ui::add_visible_ui`.
* Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)).
* Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)).
* Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)).
* Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)).
* Added `ui.weak(text)`.
* Added `Slider::step_by` ([1225](https://github.com/emilk/egui/pull/1225)).
* Added `Context::move_to_top` and `Context::top_most_layer` for managing the layer on the top ([#1242](https://github.com/emilk/egui/pull/1242)).
* Support a subset of macOS' emacs input field keybindings in `TextEdit` ([#1243](https://github.com/emilk/egui/pull/1243)).
* Added ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)).
* Added `Ui::scroll_to_rect` ([1252](https://github.com/emilk/egui/pull/1252)).
### Changed 🔧
* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding!
* `if let Some(pos) = ui.input().pointer.latest_pos()` and similar must now be rewritten on two lines.
* Search for this problem in your code using the regex `if let .*input`.
* Better contrast in the default light mode style ([#1238](https://github.com/emilk/egui/pull/1238)).
* Renamed `CtxRef` to `Context` ([#1050](https://github.com/emilk/egui/pull/1050)).
* `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)).
* Renamed `Ui::visible` to `Ui::is_visible`.
* Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)).
* Replaced `Style::body_text_style` with more generic `Style::text_styles` ([#1154](https://github.com/emilk/egui/pull/1154)).
* `TextStyle` is no longer `Copy` ([#1154](https://github.com/emilk/egui/pull/1154)).
* Replaced `TextEdit::text_style` with `TextEdit::font` ([#1154](https://github.com/emilk/egui/pull/1154)).
* `Plot::highlight` now takes a `bool` argument ([#1159](https://github.com/emilk/egui/pull/1159)).
* `ScrollArea::show` now returns a `ScrollAreaOutput`, so you might need to add `.inner` after the call to it ([#1166](https://github.com/emilk/egui/pull/1166)).
* Replaced `corner_radius: f32` with `rounding: Rounding`, allowing per-corner rounding settings ([#1206](https://github.com/emilk/egui/pull/1206)).
* Replaced Frame's `margin: Vec2` with `margin: Margin`, allowing for different margins on opposing sides ([#1219](https://github.com/emilk/egui/pull/1219)).
* Renamed `Plot::custom_label_func` to `Plot::label_formatter` ([#1235](https://github.com/emilk/egui/pull/1235)).
* `Areas::layer_id_at` ignores non-interatable layers (i.e. Tooltips) ([#1240](https://github.com/emilk/egui/pull/1240)).
* `ScrollArea`s will not shrink below a certain minimum size, set by `min_scrolled_width/min_scrolled_height` ([1255](https://github.com/emilk/egui/pull/1255)).
* For integrations:
* `Output` has now been renamed `PlatformOutput` and `Context::run` now returns the new `FullOutput` ([#1292](https://github.com/emilk/egui/pull/1292)).
* `FontImage` has been replaced by `TexturesDelta` (found in `FullOutput`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)).
* The painter must support partial texture updates ([#1149](https://github.com/emilk/egui/pull/1149)).
* Added `RawInput::max_texture_side` which should be filled in with e.g. `GL_MAX_TEXTURE_SIZE` ([#1154](https://github.com/emilk/egui/pull/1154)).
### Fixed 🐛
* Plot `Orientation` was not public, although fields using this type were ([#1130](https://github.com/emilk/egui/pull/1130)).
* Context menus now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)).
* Calling `Context::set_pixels_per_point` before the first frame will now work.
* Tooltips that don't fit the window don't flicker anymore ([#1240](https://github.com/emilk/egui/pull/1240)).
* Scroll areas now follow text cursor ([#1252](https://github.com/emilk/egui/pull/1252)).
* Slider: correctly respond with drag and focus events when interacting with the value directly ([1270](https://github.com/emilk/egui/pull/1270)).
### Contributors 🙏
* [4JX](https://github.com/4JX)
* [55nknown](https://github.com/55nknown)
* [AlanRace](https://github.com/AlanRace)
* [AlexxxRu](https://github.com/AlexxxRu)
* [awaken1ng](https://github.com/awaken1ng)
* [BctfN0HUK7Yg](https://github.com/BctfN0HUK7Yg)
* [Bromeon](https://github.com/Bromeon)
* [cat-state](https://github.com/cat)
* [danielkeller](https://github.com/danielkeller)
* [dvec](https://github.com/dvec)
* [Friz64](https://github.com/Friz64)
* [Gordon01](https://github.com/Gordon01)
* [HackerFoo](https://github.com/HackerFoo)
* [juancampa](https://github.com/juancampa)
* [justinj](https://github.com/justinj)
* [lampsitter](https://github.com/lampsitter)
* [LordMZTE](https://github.com/LordMZTE)
* [manuel-i](https://github.com/manuel)
* [Mingun](https://github.com/Mingun)
* [niklaskorz](https://github.com/niklaskorz)
* [nongiach](https://github.com/nongiach)
* [parasyte](https://github.com/parasyte)
* [psiphi75](https://github.com/psiphi75)
* [s-nie](https://github.com/s)
* [t18b219k](https://github.com/t18b219k)
* [terhechte](https://github.com/terhechte)
* [xudesheng](https://github.com/xudesheng)
* [yusdacra](https://github.com/yusdacra)
## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame`
### Added ⭐
* Added back `CtxRef::begin_frame,end_frame` as an alternative to `CtxRef::run`.
## 0.16.0 - 2021-12-29 - Context menus and rich text
### Added ⭐
* Added context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
* Most widgets containing text (`Label`, `Button` etc) now supports rich text ([#855](https://github.com/emilk/egui/pull/855)).
* Plots:
* Added bar charts and box plots ([#863](https://github.com/emilk/egui/pull/863)).
* You can now query information about the plot (e.g. get the mouse position in plot coordinates, or the plot
bounds) while adding items. `Plot` ([#766](https://github.com/emilk/egui/pull/766) and
[#892](https://github.com/emilk/egui/pull/892)).
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
* When using a custom font you can now specify a font index ([#873](https://github.com/emilk/egui/pull/873)).
* Added vertical sliders with `Slider::new(…).vertical()` ([#875](https://github.com/emilk/egui/pull/875)).
* Added `Button::image_and_text` ([#832](https://github.com/emilk/egui/pull/832)).
* Added `CollapsingHeader::open` to control if it is open or collapsed ([#1006](https://github.com/emilk/egui/pull/1006)).
* Added `egui::widgets::color_picker::color_picker_color32` to show the color picker.
### Changed 🔧
* MSRV (Minimum Supported Rust Version) is now `1.56.0`.
* `ui.add(Button::new("…").text_color(…))` is now `ui.button(RichText::new("…").color(…))` (same for `Label` )([#855](https://github.com/emilk/egui/pull/855)).
* Plots now provide a `show` method that has to be used to add items to and show the plot ([#766](https://github.com/emilk/egui/pull/766)).
* `menu::menu(ui, ...)` is now `ui.menu_button(...)` ([#543](https://github.com/emilk/egui/pull/543))
* Replaced `CtxRef::begin_frame` and `end_frame` with `CtxRef::run` ([#872](https://github.com/emilk/egui/pull/872)).
* Replaced `scroll_delta` and `zoom_delta` in `RawInput` with `Event::Scroll` and `Event::Zoom`.
* Unified the four `Memory` data buckets (`data`, `data_temp`, `id_data` and `id_data_temp`) into a single `Memory::data`, with a new interface ([#836](https://github.com/emilk/egui/pull/836)).
* Replaced `Ui::__test` with `egui::__run_test_ui` ([#872](https://github.com/emilk/egui/pull/872)).
### Fixed 🐛
* Fixed `ComboBox` and other popups getting clipped to parent window ([#885](https://github.com/emilk/egui/pull/885)).
* The color picker is now better at keeping the same hue even when saturation goes to zero ([#886](https://github.com/emilk/egui/pull/886)).
### Removed 🔥
* Removed `egui::math` (use `egui::emath` instead).
* Removed `egui::paint` (use `egui::epaint` instead).
### Contributors 🙏
* [5225225](https://github.com/5225225): [#849](https://github.com/emilk/egui/pull/849).
* [aevyrie](https://github.com/aevyrie): [#966](https://github.com/emilk/egui/pull/966).
* [B-Reif](https://github.com/B-Reif): [#875](https://github.com/emilk/egui/pull/875).
* [Bromeon](https://github.com/Bromeon): [#863](https://github.com/emilk/egui/pull/863), [#918](https://github.com/emilk/egui/pull/918).
* [d10sfan](https://github.com/d10sfan): [#832](https://github.com/emilk/egui/pull/832).
* [EmbersArc](https://github.com/EmbersArc): [#766](https://github.com/emilk/egui/pull/766), [#892](https://github.com/emilk/egui/pull/892).
* [Hperigo](https://github.com/Hperigo): [#905](https://github.com/emilk/egui/pull/905).
* [isegal](https://github.com/isegal): [#934](https://github.com/emilk/egui/pull/934).
* [mankinskin](https://github.com/mankinskin): [#543](https://github.com/emilk/egui/pull/543).
* [niladic](https://github.com/niladic): [#499](https://github.com/emilk/egui/pull/499), [#863](https://github.com/emilk/egui/pull/863).
* [singalen](https://github.com/singalen): [#973](https://github.com/emilk/egui/pull/973).
* [sumibi-yakitori](https://github.com/sumibi-yakitori): [#830](https://github.com/emilk/egui/pull/830), [#870](https://github.com/emilk/egui/pull/870).
* [t18b219k](https://github.com/t18b219k): [#868](https://github.com/emilk/egui/pull/868), [#888](https://github.com/emilk/egui/pull/888).
## 0.15.0 - 2021-10-24 - Syntax highlighting and hscroll
<img src="media/egui-0.15-code-editor.gif">
### Added ⭐
* Added horizontal scrolling support to `ScrollArea` and `Window` (opt-in).
* `TextEdit::layouter`: Add custom text layout for e.g. syntax highlighting or WYSIWYG.
* `Fonts::layout_job`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough.
* Added `ui.add_enabled(bool, widget)` to easily add a possibly disabled widget.
* Added `ui.add_enabled_ui(bool, |ui| …)` to create a possibly disabled UI section.
* Added feature `"serialize"` separatedly from `"persistence"`.
* Added `egui::widgets::global_dark_light_mode_buttons` to easily add buttons for switching the egui theme.
* `TextEdit` can now be used to show text which can be selected and copied, but not edited.
* Added `Memory::caches` for caching things from one frame to the next.
### Changed 🔧
* Change the default monospace font to [Hack](https://github.com/source-foundry/Hack).
* Label text will now be centered, right-aligned and/or justified based on the layout of the `Ui` it is in.
* `Hyperlink` will now word-wrap just like a `Label`.
* All `Ui`s must now have a finite `max_rect`.
* Deprecated: `max_rect_finite`, `available_size_before_wrap_finite` and `available_rect_before_wrap_finite`.
* `Painter`/`Fonts`: text layout now expect a color when creating a `Galley`. You may override that color with `Painter::galley_with_color`.
* MSRV (Minimum Supported Rust Version) is now `1.54.0`.
* By default, `DragValue`s no longer show a tooltip when hovered. Change with `Style::explanation_tooltips`.
* Smaller and nicer color picker.
* `ScrollArea` will auto-shrink to content size unless told otherwise using `ScollArea::auto_shrink`.
* By default, `Slider`'s `clamp_to_range` is set to true.
* Renamed `TextEdit::enabled` to `TextEdit::interactive`.
* `ui.label` (and friends) now take `impl ToString` as argument instead of `impl Into<Label>`.
### Fixed 🐛
* Fixed wrongly sized multiline `TextEdit` in justified layouts.
* Fixed clip rectangle of windows that don't fit the central area.
* Show tooltips above widgets on touch screens.
* Fixed popups sometimes getting clipped by panels.
### Removed 🔥
* Replace `Button::enabled` with `ui.add_enabled`.
### Contributors 🙏
* [AlexApps99](https://github.com/AlexApps99)
* [baysmith](https://github.com/baysmith)
* [bpostlethwaite](https://github.com/bpostlethwaite)
* [cwfitzgerald](https://github.com/cwfitzgerald)
* [DrOptix](https://github.com/DrOptix)
* [JerzySpendel](https://github.com/JerzySpendel)
* [NiceneNerd](https://github.com/NiceneNerd)
* [parasyte](https://github.com/parasyte)
* [spersson](https://github.com/spersson)
* [Stock84-dev](https://github.com/Stock84-dev)
* [sumibi-yakitori](https://github.com/sumibi-yakitori)
* [t18b219k](https://github.com/t18b219k)
* [TobTobXX](https://github.com/TobTobXX)
* [zu1k](https://github.com/zu1k)
## 0.14.2 - 2021-08-28 - Window resize fix
### Fixed 🐛
* Fixed window resize bug introduced in `0.14.1`.
## 0.14.1 - 2021-08-28 - Layout bug fixes
### Added ⭐
* Added `Ui::horizontal_top`.
### Fixed 🐛
* Fixed `set_width/set_min_width/set_height/set_min_height/expand_to_include_x/expand_to_include_y`.
* Make minimum grid column width propagate properly.
* Make sure `TextEdit` contents expand to fill width if applicable.
* `ProgressBar`: add a minimum width and fix for having it in an infinite layout.
* Fixed sometimes not being able to click inside a combo box or popup menu.
## 0.14.0 - 2021-08-24 - Ui panels and bug fixes
### Added ⭐
* Panels can now be added to any `Ui`.
* Plot:
* [Line styles](https://github.com/emilk/egui/pull/482).
* Added `show_background` and `show_axes` methods to `Plot`.
* [Progress bar](https://github.com/emilk/egui/pull/519).
* `Grid::num_columns`: allow the last column to take up the rest of the space of the parent `Ui`.
* Added an API for dropping files into egui (see `RawInput`).
* `CollapsingHeader` can now optionally be selectable.
### Changed 🔧
* A single-line `TextEdit` will now clip text that doesn't fit in it, and scroll.
* Return closure return value from `Area::show`, `ComboBox::show_ui`, `ComboBox::combo_box_with_label`, `Window::show`, `popup::*`, `menu::menu`.
* Only move/resize windows with primary mouse button.
* Tooltips are now moved to not cover the widget they are attached to.
### Fixed 🐛
* Fixed custom font definitions getting replaced when `pixels_per_point` is changed.
* Fixed `lost_focus` for `TextEdit`.
* Clicking the edge of a menu button will now properly open the menu.
* Fixed hover detection close to an `Area`.
* Fixed case where `Plot`'s `min_auto_bounds` could be ignored after the first call to `Plot::ui`.
* Fixed slow startup when using large font files.
### Contributors 🙏
* [barrowsys](https://github.com/barrowsys)
* [EmbersArc](https://github.com/EmbersArc)
* [gents83](https://github.com/gents83 )
* [lucaspoffo](https://github.com/lucaspoffo)
* [mankinskin](https://github.com/mankinskin)
* [mental32](https://github.com/mental32)
* [mitchmindtree](https://github.com/mitchmindtree)
* [parasyte](https://github.com/parasyte)
* [rekka](https://github.com/rekka)
* [zu1k](https://github.com/zu1k)
## 0.13.1 - 2021-06-28 - Plot fixes
### Added ⭐
* Plot: you can now set the stroke of a `HLine/VLine`.
### Changed 🔧
* `Plot::new` now takes an `id_source: impl Hash` instead of a `name: impl ToString`. Functionally it is the same.
## 0.13.0 - 2021-06-24 - Better panels, plots and new visual style
### Added ⭐
* Plot:
* [More plot items: Arrows, Polygons, Text, Images](https://github.com/emilk/egui/pull/471).
* [Plot legend improvements](https://github.com/emilk/egui/pull/410).
* [Line markers for plots](https://github.com/emilk/egui/pull/363).
* Panels:
* Added right and bottom panels (`SidePanel::right` and `Panel::bottom`).
* Panels can now be resized.
* Added an option to overwrite frame of a `Panel`.
* [Improve accessibility / screen reader](https://github.com/emilk/egui/pull/412).
* Added `ScrollArea::show_rows` for efficient scrolling of huge UI:s.
* Added `ScrollArea::enable_scrolling` to allow freezing scrolling when editing TextEdit widgets within it
* Added `Ui::set_visible` as a way to hide widgets.
* Added `Style::override_text_style` to easily change the text style of everything in a `Ui` (or globally).
* You can now change `TextStyle` on checkboxes, radio buttons and `SelectableLabel`.
* Added support for [cint](https://crates.io/crates/cint) under `cint` feature.
* Added features `extra_asserts` and `extra_debug_asserts` to enable additional checks.
* `TextEdit` now supports edits on a generic buffer using `TextBuffer`.
* Added `Context::set_debug_on_hover` and `egui::trace!(ui)`
### Changed 🔧
* Minimum Rust version is now 1.51 (used to be 1.52)
* [Tweaked the default visuals style](https://github.com/emilk/egui/pull/450).
* Plot: Renamed `Curve` to `Line`.
* `TopPanel::top` is now `TopBottomPanel::top`.
* `SidePanel::left` no longet takes the default width by argument, but by a builder call.
* `SidePanel::left` is resizable by default.
### Fixed 🐛
* Fixed uneven lettering on non-integral device scales ("extortion lettering").
* Fixed invisible scroll bar when native window is too narrow for egui.
## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots
### Added ⭐
* Added anchors to windows and areas so you can put a window in e.g. the top right corner.
* Make labels interactive with `Label::sense(Sense::click())`.
* Added `Response::request_focus` and `Response::surrender_focus`.
* Added `TextEdit::code_editor` (VERY basic).
* [Pan and zoom plots](https://github.com/emilk/egui/pull/317).
* [Add plot legends](https://github.com/emilk/egui/pull/349).
* [Users can now store custom state in `egui::Memory`](https://github.com/emilk/egui/pull/257).
* Added `Response::on_disabled_hover_text` to show tooltip for disabled widgets.
* Zoom input: ctrl-scroll and (on `eframe` web) trackpad-pinch gesture.
* Support for raw [multi touch](https://github.com/emilk/egui/pull/306) events,
enabling zoom, rotate, and more. Works with `eframe` web on mobile devices,
and should work with `egui_glium` for certain touch devices/screens.
* Added (optional) compatibility with [mint](https://docs.rs/mint).
### Changed 🔧
* Make `Memory::has_focus` public (again).
* `Plot` must now be given a name that is unique within its scope.
* Tab only selects labels if the `screen_reader` option is turned on.
* Renamed `ui.wrap` to `ui.scope`.
### Fixed 🐛
* Fixed [defocus-bug on touch screens](https://github.com/emilk/egui/issues/288).
* Fixed bug with the layout of wide `DragValue`s.
### Removed 🔥
* Moved experimental markup language to `egui_demo_lib`
## 0.11.0 - 2021-04-05 - Optimization, screen reader & new layout logic
### Added ⭐
* You can now give focus to any clickable widget with tab/shift-tab.
* Use space or enter to click the selected widget.
* Use arrow keys to adjust sliders and `DragValue`s.
* egui will now output events when widgets gain keyboard focus.
* This can be hooked up to a screen reader to aid the visually impaired
* Added the option to restrict the dragging bounds of `Window` and `Area` to a specified area using `drag_bounds(rect)`.
* Added support for small and raised text.
* Added `ui.set_row_height`.
* Added `DebugOptions::show_widgets` to debug layouting by hovering widgets.
* Added `ComboBox` to more easily customize combo boxes.
* Added `Slider::new` and `DragValue::new` to replace old type-specific constructors.
* Added `TextEdit::password` to hide input characters.
### Changed 🔧
* `ui.advance_cursor` is now called `ui.add_space`.
* `kb_focus` is now just called `focus`.
### Fixed 🐛
* Fixed some bugs related to centered layouts.
* Fixed secondary-click to open a menu.
* [Fix panic for zero-range sliders and zero-speed drag values](https://github.com/emilk/egui/pull/216).
* Fixed false id clash error for wrapping text.
* Fixed bug that would close a popup (e.g. the color picker) when clicking inside of it.
### Deprecated ☢️
* Deprectated `combo_box_with_label` in favor of new `ComboBox`.
* Deprectated type-specific constructors for `Slider` and `DragValue` (`Slider::f32`, `DragValue::usize` etc).
## 0.10.0 - 2021-02-28 - Plot and polish
<img src="media/egui-0.10-plot.gif" width="50%">
### Added ⭐
* Added `egui::plot::Plot` to plot some 2D data.
* Added `Ui::hyperlink_to(label, url)`.
* Sliders can now have a value prefix and suffix (e.g. the suffix `"°"` works like a unit).
* `Context::set_pixels_per_point` to control the scale of the UI.
* Added `Response::changed()` to query if e.g. a slider was dragged, text was entered or a checkbox was clicked.
* Added support for all integers in `DragValue` and `Slider` (except 128-bit).
### Changed 🔧
* Improve the positioning of tooltips. * Improve the positioning of tooltips.
* Only show tooltips if mouse is still. * Only show tooltips if mouse is still.
* `Slider` will now show the value display by default, unless turned off with `.show_value(false)`. * `Slider` will now show the value display by default, unless turned off with `.show_value(false)`.
* The `Slider` value is now a `DragValue` which when dragged can pick values outside of the slider range (unless `clamp_to_range` is set).
## 0.9.0 - 2021-02-07 - Light Mode and much more ## 0.9.0 - 2021-02-07 - Light Mode and much more
@ -625,33 +26,36 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
<img src="media/0.9.0-disabled.gif" width="50%"> <img src="media/0.9.0-disabled.gif" width="50%">
### Added ⭐ ### Added ⭐
* Added support for secondary and middle mouse buttons.
* Added `Label` methods for code, strong, strikethrough, underline and italics. * Add support for secondary and middle mouse buttons.
* Added `ui.group(|ui| { … })` to visually group some widgets within a frame. * Add `Label` methods for code, strong, strikethrough, underline and italics.
* Added `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more). * Add `ui.group(|ui| { … })` to visually group some widgets within a frame.
* Added `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive). * Add `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more).
* Added `TextEdit::hint_text` for showing a weak hint text when empty. * Add `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive).
* Add `TextEdit::hint_text` for showing a weak hint text when empty.
* `egui::popup::popup_below_widget`: show a popup area below another widget. * `egui::popup::popup_below_widget`: show a popup area below another widget.
* Added `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range. * Add `Slider::clamp_to_range(bool)`: if set, clamp the incoming and outgoing values to the slider range.
* Add: `ui.spacing()`, `ui.spacing_mut()`, `ui.visuals()`, `ui.visuals_mut()`. * Add: `ui.spacing()`, `ui.spacing_mut()`, `ui.visuals()`, `ui.visuals_mut()`.
* Add: `ctx.set_visuals()`. * Add: `ctx.set_visuals()`.
* You can now control text wrapping with `Style::wrap`. * You can now control text wrapping with `Style::wrap`.
* Added `Grid::max_col_width`. * Add `Grid::max_col_width`.
### Changed 🔧 ### Changed 🔧
* Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority. * Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority.
* Widgets will now always line break at `\n` characters. * Widgets will now always line break at `\n` characters.
* Widgets will now more intelligently choose whether or not to wrap text. * Widgets will now more intelligently choose wether or not to wrap text.
* `mouse` has been renamed `pointer` everywhere (to make it clear it includes touches too). * `mouse` has been renamed `pointer` everywhere (to make it clear it includes touches too).
* Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`. * Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`.
* `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead. * `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead.
* Backend: pointer (mouse/touch) position and buttons are now passed to egui in the event stream. * Backend: pointer (mouse/touch) position and buttons are now passed to egui in the event stream.
* `DragValue::range` is now called `clamp_range` and also clamps incoming values. * `DragValue::range` is now called `clamp_range` and also clamps incoming values.
* Renamed `Triangles` to `Mesh`. * Renamed `Triangles` to `Mesh`.
* The tessellator now wraps the clip rectangle and mesh in `struct ClippedMesh(Rect, Mesh)`. * The tesselator now wraps the clip rectangle and mesh in `struct ClippedMesh(Rect, Mesh)`.
* `Mesh::split_to_u16` now returns a 16-bit indexed `Mesh16`. * `Mesh::split_to_u16` now returns a 16-bit indexed `Mesh16`.
### Fixed 🐛 ### Fixed 🐛
* It is now possible to click widgets even when FPS is very low. * It is now possible to click widgets even when FPS is very low.
* Tessellator: handle sharp path corners better (switch to bevel instead of miter joints for > 90°). * Tessellator: handle sharp path corners better (switch to bevel instead of miter joints for > 90°).
@ -661,19 +65,22 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
<img src="media/widget_gallery_0.8.0.gif" width="50%"> <img src="media/widget_gallery_0.8.0.gif" width="50%">
### Added ⭐ ### Added ⭐
* Added a simple grid layout (`Grid`). * Added a simple grid layout (`Grid`).
* Added `ui.allocate_at_least` and `ui.allocate_exact_size`. * Added `ui.allocate_at_least` and `ui.allocate_exact_size`.
* Added function `InputState::key_down`. * Added function `InputState::key_down`.
* Added `Window::current_pos` to position a window. * Added `Window::current_pos` to position a window.
### Changed 🔧 ### Changed 🔧
* New simpler and sleeker look! * New simpler and sleeker look!
* Renamed `PaintCmd` to `Shape`. * Rename `PaintCmd` to `Shape`.
* Replace tuple `(Rect, Shape)` with tuple-struct `ClippedShape`. * Replace tuple `(Rect, Shape)` with tuple-struct `ClippedShape`.
* Renamed feature `"serde"` to `"persistence"`. * Rename feature `"serde"` to `"persistence"`.
* Break out the modules `math` and `paint` into separate crates `emath` and `epaint`. * Break out the modules `math` and `paint` into separate crates `emath` and `epaint`.
### Fixed 🐛 ### Fixed 🐛
* Fixed a bug that would sometimes trigger a "Mismatching panels" panic in debug builds. * Fixed a bug that would sometimes trigger a "Mismatching panels" panic in debug builds.
* `Image` and `ImageButton` will no longer stretch to fill a justified layout. * `Image` and `ImageButton` will no longer stretch to fill a justified layout.
@ -681,39 +88,45 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## 0.7.0 - 2021-01-04 ## 0.7.0 - 2021-01-04
### Added ⭐ ### Added ⭐
* Added `ui.scroll_to_cursor` and `response.scroll_to_me` ([#81](https://github.com/emilk/egui/pull/81) by [lucaspoffo](https://github.com/lucaspoffo)).
* Added `window.id(…)` and `area.id(…)` for overriding the default `Id`. * Add `ui.scroll_to_cursor` and `response.scroll_to_me` ([#81](https://github.com/emilk/egui/pull/81) by [lucaspoffo](https://github.com/lucaspoffo)).
* Add `window.id(…)` and `area.id(…)` for overriding the default `Id`.
### Changed 🔧 ### Changed 🔧
* Renamed `Srgba` to `Color32`. * Renamed `Srgba` to `Color32`.
* All color contructions now starts with `from_`, e.g. `Color32::from_rgb`. * All color contructions now starts with `from_`, e.g. `Color32::from_rgb`.
* Renamed `FontFamily::VariableWidth` to `FontFamily::Proportional`. * Renamed `FontFamily::VariableWidth` to `FontFamily::Proportional`.
* Removed `pixels_per_point` from `FontDefinitions`. * Removed `pixels_per_point` from `FontDefinitions`.
### Fixed 🐛 ### Fixed 🐛
* `RepaintSignal` now implements `Sync` so it can be sent to a background thread. * `RepaintSignal` now implements `Sync` so it can be sent to a background thread.
* `TextEdit` widgets are now slightly larger to accommodate their frames. * `TextEdit` widgets are now slightly larger to accommodate their frames.
### Deprecated ☢️ ### Deprecated
* Deprecated `color::srgba`. * Deprecated `color::srgba`.
## 0.6.0 - 2020-12-26 ## 0.6.0 - 2020-12-26
### Added ⭐ ### Added ⭐
* Turn off `Window` title bars with `window.title_bar(false)`. * Turn off `Window` title bars with `window.title_bar(false)`.
* `ImageButton` - `ui.add(ImageButton::new())`. * `ImageButton` - `ui.add(ImageButton::new(...))`.
* `ui.vertical_centered` and `ui.vertical_centered_justified`. * `ui.vertical_centered` and `ui.vertical_centered_justified`.
* `ui.allocate_painter` helper. * `ui.allocate_painter` helper.
* Mouse-over explanation to duplicate ID warning. * Mouse-over explanation to duplicate ID warning.
* You can now easily constrain egui to a portion of the screen using `RawInput::screen_rect`. * You can now easily constrain egui to a portion of the screen using `RawInput::screen_rect`.
* You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`. * You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`.
* Added `egui::math::Rot2`: rotation helper. * Add `egui::math::Rot2`: rotation helper.
* `Response` now contains the `Id` of the widget it pertains to. * `Response` now contains the `Id` of the widget it pertains to.
* `ui.allocate_response` that allocates space and checks for interactions. * `ui.allocate_response` that allocates space and checks for interactions.
* Added `response.interact(sense)`, e.g. to check for clicks on labels. * Add `response.interact(sense)`, e.g. to check for clicks on labels.
### Changed 🔧 ### Changed 🔧
* `ui.allocate_space` now returns an `(Id, Rect)` tuple. * `ui.allocate_space` now returns an `(Id, Rect)` tuple.
* `Arc<Context>` has been replaced with `CtxRef` everywhere. * `Arc<Context>` has been replaced with `CtxRef` everywhere.
* Slight tweak of the default `Style` and font sizes. * Slight tweak of the default `Style` and font sizes.
@ -726,14 +139,15 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* Combo boxes has scroll bars when needed. * Combo boxes has scroll bars when needed.
* Expand `Window` + `Resize` containers to be large enough for last frames content * Expand `Window` + `Resize` containers to be large enough for last frames content
* `ui.columns`: Columns now defaults to justified top-to-down layouts. * `ui.columns`: Columns now defaults to justified top-to-down layouts.
* Renamed `Sense::nothing()` to `Sense::hover()`. * Rename `Sense::nothing()` to `Sense::hover()`.
* Replaced `parking_lot` dependency with `atomic_refcell` by default. * Replaced `parking_lot` dependency with `atomic_refcell` by default.
### Fixed 🐛 ### Fixed 🐛
* The background for `CentralPanel` will now cover unused space too. * The background for `CentralPanel` will now cover unused space too.
* `ui.columns`: Improve allocated size estimation. * `ui.columns`: Improve allocated size estimation.
### Deprecated ☢️ ### Deprecated
* `RawInput::screen_size` - use `RawInput::screen_rect` instead. * `RawInput::screen_size` - use `RawInput::screen_rect` instead.
* left/centered/right column functions on `Ui`. * left/centered/right column functions on `Ui`.
* `ui.interact_hover` and `ui.hovered`. * `ui.interact_hover` and `ui.hovered`.
@ -742,19 +156,21 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## 0.5.0 - 2020-12-13 ## 0.5.0 - 2020-12-13
### Added ⭐ ### Added ⭐
* Emoji support: 1216 different emojis that work in any text. * Emoji support: 1216 different emojis that work in any text.
* The Demo app comes with a Font Book to explore the available glyphs. * The Demo app comes with a Font Book to explore the available glyphs.
* `ui.horizontal_wrapped(|ui| )`: Add widgets on a row but wrap at `max_size`. * `ui.horizontal_wrapped(|ui| ...)`: Add widgets on a row but wrap at `max_size`.
* `ui.horizontal_wrapped_for_text`: Like `ui.horizontal_wrapped`, but with spacing made for embedding text. * `ui.horizontal_wrapped_for_text`: Like `ui.horizontal_wrapped`, but with spacing made for embedding text.
* `ui.horizontal_for_text`: Like `ui.horizontal`, but with spacing made for embedding text. * `ui.horizontal_for_text`: Like `ui.horizontal`, but with spacing made for embedding text.
* `egui::Layout` now supports justified layouts where contents is _also_ centered, right-aligned, etc. * `egui::Layout` now supports justified layouts where contents is _also_ centered, right-aligned, etc.
* `ui.allocate_ui(size, |ui| )`: Easily create a child-`Ui` of a given size. * `ui.allocate_ui(size, |ui| ...)`: Easily create a child-`Ui` of a given size.
* `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): A text-button that can be selected. * `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): A text-button that can be selected.
* `ui.small_button`: A smaller button that looks good embedded in text. * `ui.small_button`: A smaller button that looks good embedded in text.
* `ui.drag_angle_tau`: For those who want to specify angles as fractions of τ (a full turn). * `ui.drag_angle_tau`: For those who want to specify angles as fractions of τ (a full turn).
* Added `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes. * Add `Resize::id_source` and `ScrollArea::id_source` to let the user avoid Id clashes.
### Changed 🔧 ### Changed 🔧
* New default font: [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu). * New default font: [Ubuntu-Light](https://fonts.google.com/specimen/Ubuntu).
* Make it simpler to override fonts in `FontDefinitions`. * Make it simpler to override fonts in `FontDefinitions`.
* Remove minimum button width. * Remove minimum button width.
@ -770,15 +186,17 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## 0.4.0 - 2020-11-28 ## 0.4.0 - 2020-11-28
### Added ⭐ ### Added ⭐
* `TextEdit` improvements: * `TextEdit` improvements:
* Much improved text editing, with better navigation and selection. * Much improved text editing, with better navigation and selection.
* Move focus between `TextEdit` widgets with tab and shift-tab. * Move focus between `TextEdit` widgets with tab and shift-tab.
* Undo edtis in a `TextEdit`. * Undo edtis in a `TextEdit`.
* You can now check if a `TextEdit` lost keyboard focus with `response.lost_focus`. * You can now check if a `TextEdit` lost keyboard focus with `response.lost_kb_focus`.
* Added `ui.text_edit_singleline` and `ui.text_edit_multiline`. * Added `ui.text_edit_singleline` and `ui.text_edit_multiline`.
* You can now debug why your `Ui` is unexpectedly wide with `ui.style_mut().debug.show_expand_width = true;` * You can now debug why your `Ui` is unexpectedly wide with `ui.style_mut().visuals.debug_expand_width = true;`
### Changed 🔧 ### Changed 🔧
* Pressing enter in a single-line `TextEdit` will now surrender keyboard focus for it. * Pressing enter in a single-line `TextEdit` will now surrender keyboard focus for it.
* You must now be explicit when creating a `TextEdit` if you want it to be singeline or multiline. * You must now be explicit when creating a `TextEdit` if you want it to be singeline or multiline.
* Improved automatic `Id` generation, making `Id` clashes less likely. * Improved automatic `Id` generation, making `Id` clashes less likely.
@ -787,20 +205,23 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* Fixed incorrect text wrapping width on radio buttons * Fixed incorrect text wrapping width on radio buttons
### Fixed 🐛 ### Fixed 🐛
* Fixed bug where a lost widget could still retain keyboard focus. * Fixed bug where a lost widget could still retain keyboard focus.
## 0.3.0 - 2020-11-07 ## 0.3.0 - 2020-11-07
### Added ⭐ ### Added ⭐
* Panels: you can now create panels using `SidePanel`, `TopPanel` and `CentralPanel`. * Panels: you can now create panels using `SidePanel`, `TopPanel` and `CentralPanel`.
* You can now override the default egui fonts. * You can now override the default egui fonts.
* Added ability to override text color with `visuals.override_text_color`. * Add ability to override text color with `visuals.override_text_color`.
* The demo now includes a simple drag-and-drop example. * The demo now includes a simple drag-and-drop example.
* The demo app now has a slider to scale all of egui. * The demo app now has a slider to scale all of egui.
### Changed 🔧 ### Changed 🔧
* `ui.horizontal(…)` etc returns `Response`.
* `ui.horizontal(...)` etc returns `Response`.
* Refactored the interface for `egui::app::App`. * Refactored the interface for `egui::app::App`.
* Windows are now constrained to the screen. * Windows are now constrained to the screen.
* `Context::begin_frame()` no longer returns a `Ui`. Instead put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. * `Context::begin_frame()` no longer returns a `Ui`. Instead put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
@ -812,7 +233,8 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* You can no longer throw windows. * You can no longer throw windows.
### Fixed 🐛 ### Fixed 🐛
* Fixed a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`).
* Fix a bug where some regions would slowly grow for non-integral scales (`pixels_per_point`).
## 0.2.0 - 2020-10-10 ## 0.2.0 - 2020-10-10
@ -824,7 +246,6 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* Optimization: coarse culling in the tessellator * Optimization: coarse culling in the tessellator
* CHANGED: switch argument order of `ui.checkbox` and `ui.radio` * CHANGED: switch argument order of `ui.checkbox` and `ui.radio`
## 0.1.4 - 2020-09-08 ## 0.1.4 - 2020-09-08
This is when I started the CHANGELOG.md, after almost two years of development. Better late than never. This is when I started the CHANGELOG.md, after almost two years of development. Better late than never.
@ -836,13 +257,3 @@ This is when I started the CHANGELOG.md, after almost two years of development.
* Regions: resizing, vertical scrolling, collapsing headers (sections) * Regions: resizing, vertical scrolling, collapsing headers (sections)
* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. * Rendering: Anti-aliased rendering of lines, circles, text and convex polygons.
* Tooltips on hover * Tooltips on hover
## Earlier:
* 2020-08-10: renamed the project to "egui"
* 2020-05-30: first release on crates.io (0.1.0)
* 2020-04-01: serious work starts (pandemic project)
* 2019-03-12: gave a talk about what would later become egui: https://www.youtube.com/watch?v=-pmwLHw5Gbs
* 2018-12-23: [initial commit](https://github.com/emilk/egui/commit/856bbf4dae4a69693a0324da34e8b0dd3754dfdf)
* 2018-11-04: started tinkering on a train

View file

@ -1,133 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[the egui discord](https://discord.gg/JFcEma9bJq).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View file

@ -1,102 +1,26 @@
# Contributing guidelines # Contributing guidelines
## Introduction You are welcome to contribute to egui via discussions, issues, pull requests and by publishing egui integrations.
`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!
/ Emil
## Discussion ## Discussion
You can ask questions, share screenshots and more at [GitHub Discussions](https://github.com/emilk/egui/discussions). You can ask questions, share screenshots and more at GitHub Discussions: https://github.com/emilk/egui/discussions
There is an `egui` discord at <https://discord.gg/vbuv9Xan65>.
There is also an egui channel on the Embark discord at <https://discord.gg/vY8ZGS292W> (NOTE: I work at [Embark](https://www.embark-studios.com/), but egui is my hobby project).
## Filing an issue ## Filing an issue
[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). Issues are for bug reports and feature requests. Issues are not for asking questions (use [Discussions](https://github.com/emilk/egui/discussions) for that).
Always make sure there is not already a similar issue to the one you are creating. Always make sure there is not already a similar issue to the one you are creating.
If you are filing a bug, please provide a way to reproduce it. If you are filing a bug, please provide a way to reproduce it.
## Making a PR ## Making a PR
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! Always file an issue (or find an existing one) and get feedback before you start working on a non-trivial pull request!
Browse through [`ARCHITECTURE.md`](https://github.com/emilk/egui/blob/master/ARCHITECTURE.md) to get a sense of how all pieces connects.
You can test your code locally by running `./sh/check.sh`.
When you have something that works, open a draft PR. You may get some helpful feedback early!
When you feel the PR is ready to go, do a self-review of the code, and then open it for review.
Please keep pull requests small and focused.
Don't worry about having many small commits in the PR - they will be squashed to one commit once merged.
Do not include the `.js` and `.wasm` build artifacts generated for building for web.
`git` is not great at storing large files like these, so we only commit a new web demo after a new egui release.
## Creating an integration for egui ## Creating an integration for egui
If you make an integration for `egui` for some engine or renderer, please share it with the world! If you make an integration for egui for some engine or renderer, please share it with the world!
I will add a link to it from the `egui` README.md so others can easily find it. I will add a link to it from the egui README.md so others can easily find it.
Read the section on integrations at <https://github.com/emilk/egui#integrations>.
## Code Conventions
Conventions unless otherwise specified:
* angles are in radians
* `Vec2::X` is right and `Vec2::Y` is down.
* `Pos2::ZERO` is left top.
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.
* Read some code before writing your own.
* Follow the `egui` code style.
* Add blank lines around all `fn`, `struct`, `enum`, etc.
* `// Comment like this.` and not `//like this`.
* Use `TODO` instead of `FIXME`.
* Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up`.
* Write idiomatic rust.
* Avoid `unsafe`.
* Avoid code that can cause panics.
* Use good names for everything.
* Add docstrings to types, `struct` fields and all `pub fn`.
* Add some example code (doc-tests).
* Before making a function longer, consider adding a helper function.
* 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.
* 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.
* Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/).
* Break the above rules when it makes sense.
### Good:
``` rust
/// The name of the thing.
fn name(&self) -> &str {
&self.name
}
fn foo(&self) {
// TODO(emilk): implement
}
```
### Bad:
``` rust
//some function
fn get_name(&self) -> &str {
&self.name
}
fn foo(&self) {
//FIXME: implement
}
```

3954
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,23 @@
[workspace] [workspace]
resolver = "2"
members = [ members = [
"crates/ecolor", "egui_demo_app",
"crates/egui_demo_app", "egui_demo_lib",
"crates/egui_demo_lib", "egui_glium",
"crates/egui_extras", "egui_web",
"crates/egui_glow", "egui",
"crates/egui-wgpu", "emath",
"crates/egui-winit", "epaint",
"crates/egui", "epi",
"crates/emath",
"crates/epaint",
"examples/*",
] ]
[patch.crates-io]
egui = { path = 'egui' }
egui_glium = { path = 'egui_glium' }
egui_web = { path = 'egui_web' }
[profile.release] [profile.release]
# lto = true # VERY slightly smaller wasm # lto = true # VERY slightly smaller wasm
# opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3` # opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3`
# opt-level = 1 # very slow and big wasm. Don't do this. # opt-level = 1 # very slow and big wasm. Don't do this.
opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'` opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
# opt-level = 3 # unecessarily large wasm for no performance gain # opt-level = 3 # unecessarily large wasm for no performance gain
# debug = true # include debug symbols, useful when profiling wasm
[profile.dev]
# Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897
# split-debuginfo = "unpacked" # faster debug builds on mac
# opt-level = 1 # Make debug builds run faster
# Optimize all dependencies even in debug builds (does not affect workspace packages):
[profile.dev.package."*"]
opt-level = 2

View file

@ -1,124 +0,0 @@
# https://github.com/ericseppanen/cargo-cranky
# cargo install cargo-cranky && cargo cranky
deny = ["unsafe_code"]
warn = [
"clippy::all",
"clippy::await_holding_lock",
"clippy::bool_to_int_with_if",
"clippy::char_lit_as_u8",
"clippy::checked_conversions",
"clippy::cloned_instead_of_copied",
"clippy::dbg_macro",
"clippy::debug_assert_with_mut_call",
"clippy::derive_partial_eq_without_eq",
"clippy::disallowed_methods",
"clippy::disallowed_script_idents",
"clippy::doc_link_with_quotes",
"clippy::doc_markdown",
"clippy::empty_enum",
"clippy::enum_glob_use",
"clippy::equatable_if_let",
"clippy::exit",
"clippy::expl_impl_clone_on_copy",
"clippy::explicit_deref_methods",
"clippy::explicit_into_iter_loop",
"clippy::explicit_iter_loop",
"clippy::fallible_impl_from",
"clippy::filter_map_next",
"clippy::flat_map_option",
"clippy::float_cmp_const",
"clippy::fn_params_excessive_bools",
"clippy::fn_to_numeric_cast_any",
"clippy::from_iter_instead_of_collect",
"clippy::if_let_mutex",
"clippy::implicit_clone",
"clippy::imprecise_flops",
"clippy::index_refutable_slice",
"clippy::inefficient_to_string",
"clippy::invalid_upcast_comparisons",
"clippy::iter_not_returning_iterator",
"clippy::iter_on_empty_collections",
"clippy::iter_on_single_items",
"clippy::large_digit_groups",
"clippy::large_stack_arrays",
"clippy::large_types_passed_by_value",
"clippy::let_unit_value",
"clippy::linkedlist",
"clippy::lossy_float_literal",
"clippy::macro_use_imports",
"clippy::manual_assert",
"clippy::manual_instant_elapsed",
"clippy::manual_ok_or",
"clippy::manual_string_new",
"clippy::map_err_ignore",
"clippy::map_flatten",
"clippy::map_unwrap_or",
"clippy::match_on_vec_items",
"clippy::match_same_arms",
"clippy::match_wild_err_arm",
"clippy::match_wildcard_for_single_variants",
"clippy::mem_forget",
"clippy::mismatched_target_os",
"clippy::mismatching_type_param_order",
"clippy::missing_enforced_import_renames",
"clippy::missing_errors_doc",
"clippy::missing_safety_doc",
"clippy::mut_mut",
"clippy::mutex_integer",
"clippy::needless_borrow",
"clippy::needless_continue",
"clippy::needless_for_each",
"clippy::needless_pass_by_value",
"clippy::negative_feature_names",
"clippy::nonstandard_macro_braces",
"clippy::option_option",
"clippy::path_buf_push_overwrite",
"clippy::ptr_as_ptr",
"clippy::rc_mutex",
"clippy::ref_option_ref",
"clippy::rest_pat_in_fully_bound_structs",
"clippy::same_functions_in_if_condition",
"clippy::semicolon_if_nothing_returned",
"clippy::single_match_else",
"clippy::str_to_string",
"clippy::string_add_assign",
"clippy::string_add",
"clippy::string_lit_as_bytes",
"clippy::string_to_string",
"clippy::todo",
"clippy::trailing_empty_array",
"clippy::trait_duplication_in_bounds",
"clippy::unimplemented",
"clippy::unnecessary_wraps",
"clippy::unnested_or_patterns",
"clippy::unused_peekable",
"clippy::unused_rounding",
"clippy::unused_self",
"clippy::useless_transmute",
"clippy::verbose_file_reads",
"clippy::zero_sized_map_values",
"elided_lifetimes_in_paths",
"future_incompatible",
"nonstandard_style",
"rust_2018_idioms",
"rust_2021_prelude_collisions",
"rustdoc::missing_crate_level_docs",
"semicolon_in_expressions_from_macros",
"trivial_numeric_casts",
"unused_extern_crates",
"unused_import_braces",
"unused_lifetimes",
]
allow = [
"clippy::manual_range_contains", # This one is just annoying
# Some of these we should try to put in "warn":
"clippy::type_complexity",
"clippy::undocumented_unsafe_blocks",
"trivial_casts",
"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
"unused_qualifications",
]

View file

@ -1,4 +1,4 @@
Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com> Copyright (c) 2021 Emil Ernerfeldt
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

297
README.md
View file

@ -1,39 +1,53 @@
# 🖌 egui: an easy-to-use GUI in pure Rust # 🖌 egui: an easy-to-use GUI in pure Rust
[<img alt="github" src="https://img.shields.io/badge/github-emilk/egui-8da0cb?logo=github" height="20">](https://github.com/emilk/egui)
[![Latest version](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui) [![Latest version](https://img.shields.io/crates/v/egui.svg)](https://crates.io/crates/egui)
[![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui) [![Documentation](https://docs.rs/egui/badge.svg)](https://docs.rs/egui)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
[![Build Status](https://github.com/emilk/egui/workflows/CI/badge.svg)](https://github.com/emilk/egui/actions?workflow=CI) [![Build Status](https://github.com/emilk/egui/workflows/CI/badge.svg)](https://github.com/emilk/egui/actions?workflow=CI)
[![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-MIT) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
[![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) **dependencies**: [`rusttype`](https://crates.io/crates/rusttype) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ahash`](https://crates.io/crates/ahash)
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon). egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and in your favorite game engine (or will soon).
egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. egui aims to be the easiest-to-use Rust GUI libary, and the simplest way to make a web app in Rust.
egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice. egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice.
Sections: Sections:
* [Example](#example)
* [Quick start](#quick-start) * [Quick start](#quick-start)
* [Demo](#demo) * [Demo](#demo)
* [Goals](#goals) * [Goals](#goals)
* [Who is egui for?](#who-is-egui-for) * [Who is egui for?](#who-is-egui-for)
* [State / features](#state) * [State / features](#state)
* [How it works](#how-it-works)
* [Integrations](#integrations) * [Integrations](#integrations)
* [Why immediate mode](#why-immediate-mode) * [Why immediate mode](#why-immediate-mode)
* [FAQ](#faq)
* [Other](#other) * [Other](#other)
* [Credits](#credits)
([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md)) ## Quick start
## Example If you just want to write a GUI application in Rust (for the web or for native), go to <https://github.com/emilk/egui_template/> and follow the instructions there!
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [Discussions](https://github.com/emilk/egui/discussions). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
## Demo
[Click to run egui web demo](https://emilk.github.io/egui/index.html) (works in any browser with WASM and WebGL support).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is currently using [`glium`](https://github.com/glium/glium) ([though there are plans to change that](https://github.com/emilk/egui/issues/93)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev`
**NOTE**: egui itself is completely platform agnostic.
### Example
``` rust ``` rust
ui.heading("My egui Application"); ui.heading("My egui Application");
@ -41,42 +55,18 @@ ui.horizontal(|ui| {
ui.label("Your name: "); ui.label("Your name: ");
ui.text_edit_singleline(&mut name); ui.text_edit_singleline(&mut name);
}); });
ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); ui.add(egui::Slider::u32(&mut age, 0..=120).text("age"));
if ui.button("Click each year").clicked() { if ui.button("Click each year").clicked() {
age += 1; age += 1;
} }
ui.label(format!("Hello '{}', age {}", name, age)); ui.label(format!("Hello '{}', age {}", name, age));
``` ```
<img src="media/demo.gif"> <img src="media/demo-2021-01-17.gif">
## Quick start
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/master/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/discussions). There is also [an egui discord server](https://discord.gg/JFcEma9bJq). If you want to contribute to egui, please read the [Contributing Guidelines](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md).
## Demo
[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
The native backend is [`egui_glow`](https://github.com/emilk/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:
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
## Goals ## Goals
* The easiest to use GUI library * The easiest to use GUI libary
* Responsive: target 60 Hz in debug build * Responsive: target 60 Hz in debug build
* Friendly: difficult to make mistakes, and shouldn't panic * Friendly: difficult to make mistakes, and shouldn't panic
* Portable: the same code works on the web and as a native app * Portable: the same code works on the web and as a native app
@ -84,10 +74,10 @@ On Fedora Rawhide you need to run:
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)). * A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
* No callbacks * No callbacks
* Pure immediate mode * Pure immediate mode
* Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
* Modular: You should be able to use small parts of egui and combine them in new ways * Modular: You should be able to use small parts of egui and combine them in new ways
* Safe: there is no `unsafe` code in egui * Safe: there is no `unsafe` code in egui
* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot) * Minimal dependencies: [`rusttype`](https://crates.io/crates/rusttype), [`atomic_refcell`](https://crates.io/crates/atomic_refcell) and [`ahash`](https://crates.io/crates/ahash).
egui is *not* a framework. egui is a library you call into, not an environment you program for. egui is *not* a framework. egui is a library you call into, not an environment you program for.
@ -95,7 +85,7 @@ egui is *not* a framework. egui is a library you call into, not an environment y
### Non-goals ### Non-goals
* Become the most powerful GUI library * Become the most powerful GUI libary
* Native looking interface * Native looking interface
* Advanced and flexible layouts (that's fundamentally incompatible with immediate mode) * Advanced and flexible layouts (that's fundamentally incompatible with immediate mode)
@ -109,12 +99,12 @@ But if you are writing something interactive in Rust that needs a simple GUI, eg
### egui vs Dear ImGui ### egui vs Dear ImGui
The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-rs), the Rust wrapper around the C++ library [Dear ImGui](https://github.com/ocornut/imgui). Dear ImGui is a great library (and the main inspiration for egui), with a lot more features and polish. However, egui provides some benefits for Rust users: The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-rs), the Rust wrapper around the C++ library [Dear ImGui](https://github.com/ocornut/imgui). Dear ImGui is a great library, which a lot more features and polish compared to egui. However, egui provides some benefits for Rust users:
* egui is pure Rust * egui is pure Rust
* egui is easily compiled to WASM * egui is easily compiled to WASM
* egui lets you use native Rust string types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings) * egui lets you use native Rust String types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings)
* [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs)
egui also tries to improve your experience in other small ways: egui also tries to improve your experience in other small ways:
@ -142,89 +132,67 @@ egui is in active development. It works well for what it does, but it lacks many
* Tooltips on hover * Tooltips on hover
* More * More
<img src="media/widget_gallery.gif" width="50%"> <img src="media/widget_gallery_0.8.0.gif" width="50%">
Light Theme: ## How it works
<img src="media/light_theme.png" width="50%"> Loop:
* Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* Run application code (Immediate Mode GUI)
* Tell egui to tessellate the frame graphics to a triangle mesh
* Render the triangle mesh with your favorite graphics API (see [OpenGL example](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs))
## Integrations ## Integrations
egui is built to be easy to integrate into any existing game engine or platform you are working on. egui is build to be easy to integrate into any existing game engine or platform you are working on.
egui itself doesn't know or care on what OS it is running or how to render things to the screen - that is the job of the egui integration. egui itself doesn't know or care on what OS it is running or how to render things to the screen - that is the job of the egui integration.
The integration needs to do two things:
An integration needs to do the following each frame: * **IO**: Supply egui with input (mouse position, keyboard presses, ...) and handle egui output (cursor changes, copy-paste integration, ...).
* **Painting**: Render the textured triangles that egui outputs.
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui ### Official
* Run the application code
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
### Official integrations I maintain two official egui integrations:
These are the official egui integrations: * [egui_web](https://crates.io/crates/egui_web) for making a web app. Compiles to WASM, renders with WebGL. [Click to run the egui demo](https://emilk.github.io/egui/index.html).
* [egui_glium](https://crates.io/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`. The same code can be compiled to a native app or a web app.
* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps.
* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) (DEPRECATED - looking for new maintainer).
### 3rd party integrations ### 3rd party
* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/).
* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/). * [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/).
* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw). * [`egui_winit_ash_vk_mem`](https://crates.io/crates/egui_winit_ash_vk_mem) for for [winit](https://github.com/rust-windowing/winit), [ash](https://github.com/MaikKlein/ash) and [vk_mem](https://github.com/gwihlidal/vk-mem-rs).
* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin). * [`wgpu`](https://crates.io/crates/wgpu) WebGPU API wrapper:
* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2). * [egui_wgpu_backend](https://crates.io/crates/egui_wgpu_backend) with [example code](https://github.com/hasenbanck/egui_example)
* [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2). * Alternative: [egui_winit_wgpu](https://github.com/Gonkalbell/egui_winit_wgpu) (not available to crates.io)
* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano). * [egui_sdl2_gl](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2)
* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano). * [emigui-miniquad](https://github.com/not-fl3/emigui-miniquad): backend for [Miniquad](https://github.com/not-fl3/miniquad). [Web demo](https://not-fl3.github.io/miniquad-samples/emigui.html) and [demo source](https://github.com/not-fl3/good-web-game/blob/master/examples/emigui.rs).
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad). * [egui_winit_platform](https://github.com/hasenbanck/egui_winit_platform) provides bindings between [winit](https://crates.io/crates/winit) and egui. It only provides the first half of an egui integration (IO). Painting can be done with e.g. [egui_wgpu_backend](https://crates.io/crates/egui_wgpu_backend).
* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad).
* [`egui_speedy2d`](https://github.com/heretik31/egui_speedy2d) for [Speedy2d](https://github.com/QuantumBadger/Speedy2D).
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash).
* [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs).
* [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework.
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
* [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc).
* [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan).
* [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13).
* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe).
* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/).
* [`tauri-egui`](https://github.com/tauri-apps/tauri-egui) for [tauri](https://github.com/tauri-apps/tauri).
Missing an integration for the thing you're working on? Create one, it's easy!
### Writing your own egui integration ### Writing your own egui integration
You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this: You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html), paint [`egui::ClippedMesh`](https://docs.rs/epaint/):es and handle [`egui::Output`](https://docs.rs/egui/latest/egui/struct.Output.html). The basic structure is this:
``` rust ``` rust
let mut egui_ctx = egui::CtxRef::default(); let mut egui_ctx = egui::Context::new();
// Game loop: // Game loop:
loop { loop {
// Gather input (mouse, touches, keyboard, screen size, etc):
let raw_input: egui::RawInput = my_integration.gather_input(); let raw_input: egui::RawInput = my_integration.gather_input();
let full_output = egui_ctx.run(raw_input, |egui_ctx| { egui_ctx.begin_frame(raw_input);
my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here my_app.ui(&mut egui_ctx); // add panels, windows and widgets to `egui_ctx` here
}); let (output, shapes) = egui_ctx.end_frame();
let clipped_primitives = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint let clipped_meshes = egui_ctx.tessellate(shapes); // create triangles to paint
my_integration.paint(clipped_meshes);
my_integration.paint(&full_output.textures_delta, clipped_primitives); my_integration.set_cursor_icon(output.cursor_icon);
// Also see `egui::Output` for more
let platform_output = full_output.platform_output;
my_integration.set_cursor_icon(platform_output.cursor_icon);
if !platform_output.copied_text.is_empty() {
my_integration.set_clipboard_text(platform_output.copied_text);
}
// See `egui::FullOutput` and `egui::PlatformOutput` for more
} }
``` ```
For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs). For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs) or [the `egui_web` `WebGL` painter](https://github.com/emilk/egui/blob/master/egui_web/src/webgl1.rs).
### Debugging your integration ### Debugging your integration
@ -241,30 +209,27 @@ For a reference OpenGL backend, see [the `egui_glium` painter](https://github.co
* egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
* Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
* egui prefers linear color spaces for all blending so: * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
* Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). * Otherwise: remember to decode gamma in the fragment shader.
* Otherwise: remember to decode gamma in the fragment shader. * Decode the gamma of the incoming vertex colors in your vertex shader.
* Decode the gamma of the incoming vertex colors in your vertex shader. * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
* Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). * Otherwise: gamma-encode the colors before you write them again.
* Otherwise: gamma-encode the colors before you write them again.
## Why immediate mode ## Why immediate mode
`egui` is an [immediate mode GUI library](https://en.wikipedia.org/wiki/Immediate_mode_GUI), as opposed to a *retained mode* GUI library. The difference between retained mode and immediate mode is best illustrated with the example of a button: In a retained GUI you create a button, add it to some UI and install some on-click handler (callback). The button is retained in the UI, and to change the text on it you need to store some sort of reference to it. By contrast, in immediate mode you show the button and interact with it immediately, and you do so every frame (e.g. 60 times per second). This means there is no need for any on-click handler, nor to store any reference to it. In `egui` this looks like this: `if ui.button("Save file").clicked() { save(file); }`. `egui` is an [immediate mode GUI library](https://en.wikipedia.org/wiki/Immediate_mode_GUI), as opposed to a *retained mode* GUI library. The difference between retained mode and immediate mode is best illustrated with the example of a button: In a retained GUI you create a button, add it to some UI and install some on-click handler (callback). The button is retained in the UI, and to change the text on it you need to store some sort of reference to it. By contrast, in immediate mode you show the button and interact with it immediately, and you do so every frame (e.g. 60 times per second). This means there is no need for any on-click handler, nor to store any reference to it. In `egui` this looks like this: `if ui.button("Save file").clicked() { save(file); }`.
A more detailed description of immediate mode can be found [in the `egui` docs](https://docs.rs/egui/latest/egui/#understanding-immediate-mode).
There are advantages and disadvantages to both systems. There are advantages and disadvantages to both systems.
The short of it is this: immediate mode GUI libraries are easier to use, but less powerful. The short of it is this: immediate mode GUI libraries are easier to use, but less powerful.
### Advantages of immediate mode ### Advantaged of immediate mode
#### Usability #### Usability
The main advantage of immediate mode is that the application code becomes vastly simpler: The main advantage of immediate mode is that the application code becomes vastly simpler:
* You never need to have any on-click handlers and callbacks that disrupts your code flow. * You never need to have any on-click handlers and callbacks that disrupts your code flow.
* You don't have to worry about a lingering callback calling something that is gone. * You don't have to worry about a linger callback calling something that is gone.
* Your GUI code can easily live in a simple function (no need for an object just for the UI). * Your GUI code can easily live in a simple function (no need for an object just for the UI).
* You don't have to worry about app state and GUI state being out-of-sync (i.e. the GUI showing something outdated), because the GUI isn't storing any state - it is showing the latest state *immediately*. * You don't have to worry about app state and GUI state being out-of-sync (i.e. the GUI showing something outdated), because the GUI isn't storing any state - it is showing the latest state *immediately*.
@ -279,7 +244,7 @@ In immediate mode you run into a paradox: to know the size of the window, we mus
This is a fundamental shortcoming of immediate mode GUIs, and any attempt to resolve it comes with its own downsides. This is a fundamental shortcoming of immediate mode GUIs, and any attempt to resolve it comes with its own downsides.
One workaround is to store the size and use it the next frame. This produces a frame-delay for the correct layout, producing occasional flickering the first frame something shows up. `egui` does this for some things such as windows and grid layouts. One workaround is to store the size and use it the next frame. This produces a frame-delay for the correct layout, producing occational flickering the first frame something shows up. `egui` does this for some things such as windows and grid layouts.
You can also call the layout code twice (once to get the size, once to do the interaction), but that is not only more expensive, it's also complex to implement, and in some cases twice is not enough. `egui` never does this. You can also call the layout code twice (once to get the size, once to do the interaction), but that is not only more expensive, it's also complex to implement, and in some cases twice is not enough. `egui` never does this.
@ -288,9 +253,9 @@ For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so
#### CPU usage #### CPU usage
Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be layed out each frame. Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be layed out each frame.
If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement). If you design the GUI with this in mind and refrain from huge scroll areas then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement).
If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS at no extra CPU cost. If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS for no extra CPU cost.
#### IDs #### IDs
@ -298,64 +263,7 @@ There are some GUI state that you want the GUI library to retain, even in an imm
`egui` also needs to track which widget is being interacted with (e.g. which slider is being dragged). `egui` uses unique id:s for this awell, but in this case the IDs are automatically generated, so there is no need for the user to worry about it. In particular, having two buttons with the same name is no problem (this is in contrast with [`Dear ImGui`](https://github.com/ocornut/imgui)). `egui` also needs to track which widget is being interacted with (e.g. which slider is being dragged). `egui` uses unique id:s for this awell, but in this case the IDs are automatically generated, so there is no need for the user to worry about it. In particular, having two buttons with the same name is no problem (this is in contrast with [`Dear ImGui`](https://github.com/ocornut/imgui)).
Overall, ID handling is a rare inconvenience, and not a big disadvantage. Overall, ID handling is a rare invonvenience, and not a big disadvantage.
## FAQ
Also see [GitHub Discussions](https://github.com/emilk/egui/discussions/categories/q-a).
### Can I use `egui` with non-latin characters?
Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::set_fonts`.
### Can I customize the look of egui?
Yes! You can customize the colors, spacing, fonts and sizes of everything using `Context::set_style`.
Here is an example (from https://github.com/AlexxxRu/TinyPomodoro):
<img src="media/pompodoro-skin.png" width="50%">
### How do I use egui with `async`?
If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like:
* Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)). Make sure to use [`try_recv`](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html#method.try_recv) so you don't block the gui thread!
* `Arc<Mutex<Value>>` (background thread sets a value; GUI thread reads it)
* [`poll_promise::Promise`](https://docs.rs/poll-promise) (example: [`examples/download_image/`](https://github.com/emilk/egui/blob/master/examples/download_image/))
* [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.html)
* [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html)
### What about accessibility, such as screen readers?
egui includes optional support for [AccessKit](https://accesskit.dev/), which currently implements the native accessibility APIs on Windows and macOS. This feature is enabled by default in eframe. For platforms that AccessKit doesn't yet support, including web, there is an experimental built-in screen reader; in [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab.
The original discussion of accessibility in egui is at <https://github.com/emilk/egui/issues/167>. Now that AccessKit support is merged, providing a strong foundation for future accessibility work, please open new issues on specific accessibility problems.
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
`egui` is a 2D user interface library for laying out and interacting with buttons, sliders, etc.
`egui` has no idea if it is running on the web or natively, and does not know how to collect input or show things on screen.
That is the job of *the integration* or *backend*.
It is common to use `egui` from a game engine (using e.g. [`bevy_egui`](https://docs.rs/bevy_egui)),
but you can also use `egui` stand-alone using `eframe`. `eframe` has integration for web and native, and handles input and rendering.
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).
### How do I render 3D stuff in an egui area?
There are multiple ways to combine egui with 3D. The simplest way is to use a 3D library and have egui sit on top of the 3D view. See for instance [`bevy_egui`](https://github.com/mvlabat/bevy_egui) or [`three-d`](https://github.com/asny/three-d).
If you want to embed 3D into an egui view there are two options.
#### `Shape::Callback`
Examples:
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_three-d.rs>
* <https://github.com/emilk/egui/blob/master/examples/custom_3d_glow.rs>
`Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all.
#### Render-to-texture
You can also render your 3D scene to a texture and display it using [`ui.image(…)`](https://docs.rs/egui/latest/egui/struct.Ui.html#method.image). You first need to convert the native texture to an [`egui::TextureId`](https://docs.rs/egui/latest/egui/enum.TextureId.html), and how to do this depends on the integration you use.
Examples:
* Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs
* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium): <https://github.com/emilk/egui/blob/master/crates/egui_glium/examples/native_texture.rs>.
## Other ## Other
@ -364,15 +272,13 @@ Examples:
All coordinates are in screen space coordinates, with (0, 0) in the top left corner All coordinates are in screen space coordinates, with (0, 0) in the top left corner
All coordinates are in "points" which may consist of many physical pixels. All coordinates are in locial "points" which may consist of many physical pixels.
All colors have premultiplied alpha. All colors have premultiplied alpha.
egui uses the builder pattern for construction widgets. For instance: `ui.add(Label::new("Hello").text_color(RED));` I am not a big fan of the builder pattern (it is quite verbose both in implementation and in use) but until Rust has named, default arguments it is the best we can do. To alleviate some of the verbosity there are common-case helper functions, like `ui.label("Hello");`. egui uses the builder pattern for construction widgets. For instance: `ui.add(Label::new("Hello").text_color(RED));` I am not a big fan of the builder pattern (it is quite verbose both in implementation and in use) but until Rust has named, default arguments it is the best we can do. To alleviate some of the verbosity there are common-case helper functions, like `ui.label("Hello");`.
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>. Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this.
egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock.
### Inspiration ### Inspiration
@ -380,44 +286,19 @@ The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immed
### Name ### Name
The name of the library and the project is "egui" and pronounced as "e-gooey". Please don't write it as "EGUI". The name of the library and the project is "egui" and pronounced as "e-gooey".
The library was originally called "Emigui", but was renamed to "egui" in 2020. The library was originally called "Emigui", but was renamed to "egui" in 2020.
## Credits ### Credits / Licenses
egui author and maintainer: Emil Ernerfeldt [(@emilk](https://github.com/emilk)). egui author: Emil Ernerfeldt
Notable contributions by: egui is under MIT OR Apache-2.0 license.
* [@n2](https://github.com/n2): [Mobile web input and IME support](https://github.com/emilk/egui/pull/253). Fonts:
* [@optozorax](https://github.com/optozorax): [Arbitrary widget data storage](https://github.com/emilk/egui/pull/257).
* [@quadruple-output](https://github.com/quadruple-output): [Multitouch](https://github.com/emilk/egui/pull/306).
* [@EmbersArc](https://github.com/EmbersArc): [Plots](https://github.com/emilk/egui/pulls?q=+is%3Apr+author%3AEmbersArc).
* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650).
* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685).
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
* The flattening algorithm for the cubic bezier curve and quadratic bezier curve is from [lyon_geom](https://docs.rs/lyon_geom/latest/lyon_geom/)
Default fonts:
* `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License * `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License
* `Hack-Regular.ttf`: <https://github.com/source-foundry/Hack>, [MIT Licence](https://github.com/source-foundry/Hack/blob/master/LICENSE.md)
* `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL) * `NotoEmoji-Regular.ttf`: [google.com/get/noto](https://google.com/get/noto), [SIL Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
* `ProggyClean.ttf`: Copyright (c) 2004, 2005 Tristan Grimmer. MIT License. <http://www.proggyfonts.net/>
* `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence) * `Ubuntu-Light.ttf` by [Dalton Maag](http://www.daltonmaag.com/): [Ubuntu font licence](https://ubuntu.com/legal/font-licence)
---
<div align="center">
<img src="media/rerun_io_logo.png" width="50%">
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing<br>
visualizations for computer vision and robotics.
</div>

View file

@ -1,46 +0,0 @@
# This is a configuration file for the bacon tool
# More info at https://github.com/Canop/bacon
default_job = "cranky"
[jobs]
[jobs.cranky]
command = ["cargo", "cranky", "--all-targets", "--all-features", "--color", "always"]
need_stdout = false
watch = ["tests", "benches", "examples"]
[jobs.test]
command = ["cargo", "test", "--color", "always"]
need_stdout = true
watch = ["tests"]
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--all-features", "--no-deps"]
need_stdout = false
# if the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--all-features", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate. You can run an example the same
# way. Don't forget the `--color always` part or the errors won't be
# properly parsed.
[jobs.run]
command = ["cargo", "run", "--color", "always"]
need_stdout = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal prefs.toml file instead.
[keybindings]
i = "job:initial"
c = "job:cranky"
d = "job:doc-open"
t = "job:test"
r = "job:run"

4
build_demo_native.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
set -eu
cargo run --bin egui_demo_app --release --all-features

25
build_demo_web.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
set -eu
CRATE_NAME="egui_demo_app"
# This is required to enable the web_sys clipboard API which egui_web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
export RUSTFLAGS=--cfg=web_sys_unstable_apis
# Clear output from old stuff:
rm -f docs/${CRATE_NAME}_bg.wasm
echo "Building rust…"
BUILD=release
cargo build --release --all-features -p ${CRATE_NAME} --lib --target wasm32-unknown-unknown
echo "Generating JS bindings for wasm…"
TARGET_NAME="${CRATE_NAME}.wasm"
wasm-bindgen "target/wasm32-unknown-unknown/$BUILD/$TARGET_NAME" \
--out-dir docs --no-modules --no-typescript
echo "Finished: docs/${CRATE_NAME}.wasm"
open http://localhost:8888/index.html

21
check.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/bash
set -eu
cargo check --workspace --all-targets
cargo check --workspace --all-targets --all-features
cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown
cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
cargo fmt --all -- --check
CARGO_INCREMENTAL=0 cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::all
cargo test --workspace --all-targets --all-features
cargo test --workspace --doc
# TODO: doesn't error, but at least prints a warning.
cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui_glium --lib --no-deps
cargo doc -p egui_web --target wasm32-unknown-unknown --lib --no-deps
# For finding bloat:
# cargo bloat --release --bin demo_glium -n 200 | rg egui
# what compiles slowly?
# cargo clean; cargo +nightly build -p egui -Z timings

View file

@ -1 +0,0 @@
doc-valid-idents = ["AccessKit", ".."]

View file

@ -1,13 +0,0 @@
# Changelog for ecolor
All notable changes to the `ecolor` crate will be noted in this file.
## Unreleased
## 0.21.0 - 2023-02-08
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
## 0.20.0 - 2022-12-08
* Split out `ecolor` crate from `epaint`

View file

@ -1,50 +0,0 @@
[package]
name = "ecolor"
version = "0.21.0"
authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>",
]
description = "Color structs and color conversion utilities"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
[lib]
[features]
default = []
## Enable additional checks if debug assertions are enabled (debug builds).
extra_debug_asserts = []
## Always enable additional checks.
extra_asserts = []
[dependencies]
#! ### Optional dependencies
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `ecolor` types to `&[u8]`.
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
cint = { version = "0.3.1", optional = true }
## Enable the [`hex_color`] macro.
color-hex = { version = "0.2.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = { version = "1", optional = true, features = ["derive"] }

View file

@ -1,11 +0,0 @@
# ecolor - egui color library
[![Latest version](https://img.shields.io/crates/v/ecolor.svg)](https://crates.io/crates/ecolor)
[![Documentation](https://docs.rs/ecolor/badge.svg)](https://docs.rs/ecolor)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
A simple color storage and conversion library.
Made for [`egui`](https://github.com/emilk/egui/).

View file

@ -1,161 +0,0 @@
use super::*;
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----
impl From<Alpha<EncodedSrgb<u8>>> for Color32 {
fn from(srgba: Alpha<EncodedSrgb<u8>>) -> Self {
let Alpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
Color32::from_rgba_unmultiplied(r, g, b, a)
}
}
// No From<Color32> for Alpha<_> because Color32 is premultiplied
impl From<PremultipliedAlpha<EncodedSrgb<u8>>> for Color32 {
fn from(srgba: PremultipliedAlpha<EncodedSrgb<u8>>) -> Self {
let PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
Color32::from_rgba_premultiplied(r, g, b, a)
}
}
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<u8>> {
fn from(col: Color32) -> Self {
let (r, g, b, a) = col.to_tuple();
PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
}
}
}
impl From<PremultipliedAlpha<EncodedSrgb<f32>>> for Color32 {
fn from(srgba: PremultipliedAlpha<EncodedSrgb<f32>>) -> Self {
let PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
} = srgba;
// This is a bit of an abuse of the function name but it does what we want.
let r = linear_u8_from_linear_f32(r);
let g = linear_u8_from_linear_f32(g);
let b = linear_u8_from_linear_f32(b);
let a = linear_u8_from_linear_f32(a);
Color32::from_rgba_premultiplied(r, g, b, a)
}
}
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<f32>> {
fn from(col: Color32) -> Self {
let (r, g, b, a) = col.to_tuple();
// This is a bit of an abuse of the function name but it does what we want.
let r = linear_f32_from_linear_u8(r);
let g = linear_f32_from_linear_u8(g);
let b = linear_f32_from_linear_u8(b);
let a = linear_f32_from_linear_u8(a);
PremultipliedAlpha {
color: EncodedSrgb { r, g, b },
alpha: a,
}
}
}
impl ColorInterop for Color32 {
type CintTy = PremultipliedAlpha<EncodedSrgb<u8>>;
}
// ---- Rgba ----
impl From<PremultipliedAlpha<LinearSrgb<f32>>> for Rgba {
fn from(srgba: PremultipliedAlpha<LinearSrgb<f32>>) -> Self {
let PremultipliedAlpha {
color: LinearSrgb { r, g, b },
alpha: a,
} = srgba;
Rgba([r, g, b, a])
}
}
impl From<Rgba> for PremultipliedAlpha<LinearSrgb<f32>> {
fn from(col: Rgba) -> Self {
let (r, g, b, a) = col.to_tuple();
PremultipliedAlpha {
color: LinearSrgb { r, g, b },
alpha: a,
}
}
}
impl ColorInterop for Rgba {
type CintTy = PremultipliedAlpha<LinearSrgb<f32>>;
}
// ---- Hsva ----
impl From<Alpha<Hsv<f32>>> for Hsva {
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
let Alpha {
color: Hsv { h, s, v },
alpha: a,
} = srgba;
Hsva::new(h, s, v, a)
}
}
impl From<Hsva> for Alpha<Hsv<f32>> {
fn from(col: Hsva) -> Self {
let Hsva { h, s, v, a } = col;
Alpha {
color: Hsv { h, s, v },
alpha: a,
}
}
}
impl ColorInterop for Hsva {
type CintTy = Alpha<Hsv<f32>>;
}
// ---- HsvaGamma ----
impl ColorInterop for HsvaGamma {
type CintTy = Alpha<Hsv<f32>>;
}
impl From<Alpha<Hsv<f32>>> for HsvaGamma {
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
let Alpha {
color: Hsv { h, s, v },
alpha: a,
} = srgba;
Hsva::new(h, s, v, a).into()
}
}
impl From<HsvaGamma> for Alpha<Hsv<f32>> {
fn from(col: HsvaGamma) -> Self {
let Hsva { h, s, v, a } = col.into();
Alpha {
color: Hsv { h, s, v },
alpha: a,
}
}
}

View file

@ -1,216 +0,0 @@
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba};
/// This format is used for space-efficient color representation (32 bits).
///
/// Instead of manipulating this directly it is often better
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
///
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
/// Alpha channel is in linear space.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Color32(pub(crate) [u8; 4]);
impl std::ops::Index<usize> for Color32 {
type Output = u8;
#[inline(always)]
fn index(&self, index: usize) -> &u8 {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> for Color32 {
#[inline(always)]
fn index_mut(&mut self, index: usize) -> &mut u8 {
&mut self.0[index]
}
}
impl Color32 {
// Mostly follows CSS names:
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
/// An ugly color that is planned to be replaced before making it to the screen.
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
#[inline(always)]
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 255])
}
#[inline(always)]
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 0])
}
/// From `sRGBA` with premultiplied alpha.
#[inline(always)]
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
/// From `sRGBA` WITHOUT premultiplied alpha.
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
if a == 255 {
Self::from_rgb(r, g, b) // common-case optimization
} else if a == 0 {
Self::TRANSPARENT // common-case optimization
} else {
let r_lin = linear_f32_from_gamma_u8(r);
let g_lin = linear_f32_from_gamma_u8(g);
let b_lin = linear_f32_from_gamma_u8(b);
let a_lin = linear_f32_from_linear_u8(a);
let r = gamma_u8_from_linear_f32(r_lin * a_lin);
let g = gamma_u8_from_linear_f32(g_lin * a_lin);
let b = gamma_u8_from_linear_f32(b_lin * a_lin);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
#[inline(always)]
pub const fn from_gray(l: u8) -> Self {
Self([l, l, l, 255])
}
#[inline(always)]
pub const fn from_black_alpha(a: u8) -> Self {
Self([0, 0, 0, a])
}
pub fn from_white_alpha(a: u8) -> Self {
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
}
#[inline(always)]
pub const fn from_additive_luminance(l: u8) -> Self {
Self([l, l, l, 0])
}
#[inline(always)]
pub const fn is_opaque(&self) -> bool {
self.a() == 255
}
#[inline(always)]
pub const fn r(&self) -> u8 {
self.0[0]
}
#[inline(always)]
pub const fn g(&self) -> u8 {
self.0[1]
}
#[inline(always)]
pub const fn b(&self) -> u8 {
self.0[2]
}
#[inline(always)]
pub const fn a(&self) -> u8 {
self.0[3]
}
/// Returns an opaque version of self
pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into()
}
/// Returns an additive version of self
#[inline(always)]
pub const fn additive(self) -> Self {
let [r, g, b, _] = self.to_array();
Self([r, g, b, 0])
}
/// Premultiplied RGBA
#[inline(always)]
pub const fn to_array(&self) -> [u8; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
/// Premultiplied RGBA
#[inline(always)]
pub const fn to_tuple(&self) -> (u8, u8, u8, u8) {
(self.r(), self.g(), self.b(), self.a())
}
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
Rgba::from(*self).to_srgba_unmultiplied()
}
/// Multiply with 0.5 to make color half as opaque, perceptually.
///
/// Fast multiplication in gamma-space.
///
/// This is perceptually even, and faster that [`Self::linear_multiply`].
#[inline]
pub fn gamma_multiply(self, factor: f32) -> Color32 {
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
let Self([r, g, b, a]) = self;
Self([
(r as f32 * factor + 0.5) as u8,
(g as f32 * factor + 0.5) as u8,
(b as f32 * factor + 0.5) as u8,
(a as f32 * factor + 0.5) as u8,
])
}
/// Multiply with 0.5 to make color half as opaque in linear space.
///
/// This is using linear space, which is not perceptually even.
/// You may want to use [`Self::gamma_multiply`] instead.
pub fn linear_multiply(self, factor: f32) -> Color32 {
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
// As an unfortunate side-effect of using premultiplied alpha
// we need a somewhat expensive conversion to linear space and back.
Rgba::from(self).multiply(factor).into()
}
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
///
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
/// in order to obtain linear space color values.
#[inline]
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
let Self([r, g, b, a]) = self;
[
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
]
}
}

View file

@ -1,39 +0,0 @@
/// Construct a [`crate::Color32`] from a hex RGB or RGBA string.
///
/// ```
/// # use ecolor::{hex_color, Color32};
/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22));
/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12));
/// ```
#[macro_export]
macro_rules! hex_color {
($s:literal) => {{
let array = color_hex::color_from_hex!($s);
if array.len() == 3 {
$crate::Color32::from_rgb(array[0], array[1], array[2])
} else {
#[allow(unconditional_panic)]
$crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3])
}
}};
}
#[test]
fn test_from_rgb_hex() {
assert_eq!(
crate::Color32::from_rgb(0x20, 0x21, 0x22),
hex_color!("#202122")
);
assert_eq!(
crate::Color32::from_rgb_additive(0x20, 0x21, 0x22),
hex_color!("#202122").additive()
);
}
#[test]
fn test_from_rgba_hex() {
assert_eq!(
crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50),
hex_color!("20212250")
);
}

View file

@ -1,231 +0,0 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32, Color32, Rgba,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].
/// No premultiplied alpha.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Hsva {
/// hue 0-1
pub h: f32,
/// saturation 0-1
pub s: f32,
/// value 0-1
pub v: f32,
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32,
}
impl Hsva {
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
Self { h, s, v, a }
}
/// From `sRGBA` with premultiplied alpha
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
Self::from_rgba_premultiplied(
linear_f32_from_gamma_u8(srgba[0]),
linear_f32_from_gamma_u8(srgba[1]),
linear_f32_from_gamma_u8(srgba[2]),
linear_f32_from_linear_u8(srgba[3]),
)
}
/// From `sRGBA` without premultiplied alpha
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
Self::from_rgba_unmultiplied(
linear_f32_from_gamma_u8(srgba[0]),
linear_f32_from_gamma_u8(srgba[1]),
linear_f32_from_gamma_u8(srgba[2]),
linear_f32_from_linear_u8(srgba[3]),
)
}
/// From linear RGBA with premultiplied alpha
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
if a == 0.0 {
if r == 0.0 && b == 0.0 && a == 0.0 {
Hsva::default()
} else {
Hsva::from_additive_rgb([r, g, b])
}
} else {
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
Hsva { h, s, v, a }
}
}
/// From linear RGBA without premultiplied alpha
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
let (h, s, v) = hsv_from_rgb([r, g, b]);
Hsva { h, s, v, a }
}
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva {
h,
s,
v,
a: -0.5, // anything negative is treated as additive
}
}
pub fn from_rgb(rgb: [f32; 3]) -> Self {
let (h, s, v) = hsv_from_rgb(rgb);
Hsva { h, s, v, a: 1.0 }
}
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
Self::from_rgb([
linear_f32_from_gamma_u8(r),
linear_f32_from_gamma_u8(g),
linear_f32_from_gamma_u8(b),
])
}
// ------------------------------------------------------------------------
pub fn to_opaque(self) -> Self {
Self { a: 1.0, ..self }
}
pub fn to_rgb(&self) -> [f32; 3] {
rgb_from_hsv((self.h, self.s, self.v))
}
pub fn to_srgb(&self) -> [u8; 3] {
let [r, g, b] = self.to_rgb();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
]
}
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
let additive = a < 0.0;
if additive {
[r, g, b, 0.0]
} else {
[a * r, a * g, a * b, a]
}
}
/// Represents additive colors using a negative alpha.
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
let Hsva { h, s, v, a } = *self;
let [r, g, b] = rgb_from_hsv((h, s, v));
[r, g, b, a]
}
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_premultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a),
]
}
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a.abs()),
]
}
}
impl From<Hsva> for Rgba {
fn from(hsva: Hsva) -> Rgba {
Rgba(hsva.to_rgba_premultiplied())
}
}
impl From<Rgba> for Hsva {
fn from(rgba: Rgba) -> Hsva {
Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
}
}
impl From<Hsva> for Color32 {
fn from(hsva: Hsva) -> Color32 {
Color32::from(Rgba::from(hsva))
}
}
impl From<Color32> for Hsva {
fn from(srgba: Color32) -> Hsva {
Hsva::from(Rgba::from(srgba))
}
}
/// All ranges in 0-1, rgb is linear.
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
#![allow(clippy::many_single_char_names)]
let min = r.min(g.min(b));
let max = r.max(g.max(b)); // value
let range = max - min;
let h = if max == min {
0.0 // hue is undefined
} else if max == r {
(g - b) / (6.0 * range)
} else if max == g {
(b - r) / (6.0 * range) + 1.0 / 3.0
} else {
// max == b
(r - g) / (6.0 * range) + 2.0 / 3.0
};
let h = (h + 1.0).fract(); // wrap
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
(h, s, max)
}
/// All ranges in 0-1, rgb is linear.
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
#![allow(clippy::many_single_char_names)]
let h = (h.fract() + 1.0).fract(); // wrap
let s = s.clamp(0.0, 1.0);
let f = h * 6.0 - (h * 6.0).floor();
let p = v * (1.0 - s);
let q = v * (1.0 - f * s);
let t = v * (1.0 - (1.0 - f) * s);
match (h * 6.0).floor() as i32 % 6 {
0 => [v, t, p],
1 => [q, v, p],
2 => [p, v, t],
3 => [p, q, v],
4 => [t, p, v],
5 => [v, p, q],
_ => unreachable!(),
}
}
#[test]
#[ignore] // a bit expensive
fn test_hsv_roundtrip() {
for r in 0..=255 {
for g in 0..=255 {
for b in 0..=255 {
let srgba = Color32::from_rgb(r, g, b);
let hsva = Hsva::from(srgba);
assert_eq!(srgba, Color32::from(hsva));
}
}
}
}

View file

@ -1,66 +0,0 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct HsvaGamma {
/// hue 0-1
pub h: f32,
/// saturation 0-1
pub s: f32,
/// value 0-1, in gamma-space (~perceptually even)
pub v: f32,
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
pub a: f32,
}
impl From<HsvaGamma> for Rgba {
fn from(hsvag: HsvaGamma) -> Rgba {
Hsva::from(hsvag).into()
}
}
impl From<HsvaGamma> for Color32 {
fn from(hsvag: HsvaGamma) -> Color32 {
Rgba::from(hsvag).into()
}
}
impl From<HsvaGamma> for Hsva {
fn from(hsvag: HsvaGamma) -> Hsva {
let HsvaGamma { h, s, v, a } = hsvag;
Hsva {
h,
s,
v: linear_from_gamma(v),
a,
}
}
}
impl From<Rgba> for HsvaGamma {
fn from(rgba: Rgba) -> HsvaGamma {
Hsva::from(rgba).into()
}
}
impl From<Color32> for HsvaGamma {
fn from(srgba: Color32) -> HsvaGamma {
Hsva::from(srgba).into()
}
}
impl From<Hsva> for HsvaGamma {
fn from(hsva: Hsva) -> HsvaGamma {
let Hsva { h, s, v, a } = hsva;
HsvaGamma {
h,
s,
v: gamma_from_linear(v),
a,
}
}
}

View file

@ -1,173 +0,0 @@
//! Color conversions and types.
//!
//! If you want a compact color representation, use [`Color32`].
//! If you want to manipulate RGBA colors use [`Rgba`].
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::wrong_self_convention)]
#[cfg(feature = "cint")]
mod cint_impl;
#[cfg(feature = "cint")]
pub use cint_impl::*;
mod color32;
pub use color32::*;
mod hsva_gamma;
pub use hsva_gamma::*;
mod hsva;
pub use hsva::*;
#[cfg(feature = "color-hex")]
mod hex_color_macro;
mod rgba;
pub use rgba::*;
// ----------------------------------------------------------------------------
// Color conversion:
impl From<Color32> for Rgba {
fn from(srgba: Color32) -> Rgba {
Rgba([
linear_f32_from_gamma_u8(srgba.0[0]),
linear_f32_from_gamma_u8(srgba.0[1]),
linear_f32_from_gamma_u8(srgba.0[2]),
linear_f32_from_linear_u8(srgba.0[3]),
])
}
}
impl From<Rgba> for Color32 {
fn from(rgba: Rgba) -> Color32 {
Color32([
gamma_u8_from_linear_f32(rgba.0[0]),
gamma_u8_from_linear_f32(rgba.0[1]),
gamma_u8_from_linear_f32(rgba.0[2]),
linear_u8_from_linear_f32(rgba.0[3]),
])
}
}
/// gamma [0, 255] -> linear [0, 1].
pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
if s <= 10 {
s as f32 / 3294.6
} else {
((s as f32 + 14.025) / 269.025).powf(2.4)
}
}
/// linear [0, 255] -> linear [0, 1].
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
a as f32 / 255.0
}
/// linear [0, 1] -> gamma [0, 255] (clamped).
/// Values outside this range will be clamped to the range.
pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
if l <= 0.0 {
0
} else if l <= 0.0031308 {
fast_round(3294.6 * l)
} else if l <= 1.0 {
fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
} else {
255
}
}
/// linear [0, 1] -> linear [0, 255] (clamped).
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
fast_round(a * 255.0)
}
fn fast_round(r: f32) -> u8 {
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
}
#[test]
pub fn test_srgba_conversion() {
for b in 0..=255 {
let l = linear_f32_from_gamma_u8(b);
assert!(0.0 <= l && l <= 1.0);
assert_eq!(gamma_u8_from_linear_f32(l), b);
}
}
/// gamma [0, 1] -> linear [0, 1] (not clamped).
/// Works for numbers outside this range (e.g. negative numbers).
pub fn linear_from_gamma(gamma: f32) -> f32 {
if gamma < 0.0 {
-linear_from_gamma(-gamma)
} else if gamma <= 0.04045 {
gamma / 12.92
} else {
((gamma + 0.055) / 1.055).powf(2.4)
}
}
/// linear [0, 1] -> gamma [0, 1] (not clamped).
/// Works for numbers outside this range (e.g. negative numbers).
pub fn gamma_from_linear(linear: f32) -> f32 {
if linear < 0.0 {
-gamma_from_linear(-linear)
} else if linear <= 0.0031308 {
12.92 * linear
} else {
1.055 * linear.powf(1.0 / 2.4) - 0.055
}
}
// ----------------------------------------------------------------------------
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
/// or with the `extra_debug_asserts` feature in debug builds.
#[macro_export]
macro_rules! ecolor_assert {
($($arg: tt)*) => {
if cfg!(any(
feature = "extra_asserts",
all(feature = "extra_debug_asserts", debug_assertions),
)) {
assert!($($arg)*);
}
}
}
// ----------------------------------------------------------------------------
/// Cheap and ugly.
/// Made for graying out disabled `Ui`s.
pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
let [mut r, mut g, mut b, mut a] = color.to_array();
if a == 0 {
r /= 2;
g /= 2;
b /= 2;
} else if a < 170 {
// Cheapish and looks ok.
// Works for e.g. grid stripes.
let div = (2 * 255 / a as i32) as u8;
r = r / 2 + target.r() / div;
g = g / 2 + target.g() / div;
b = b / 2 + target.b() / div;
a /= 2;
} else {
r = r / 2 + target.r() / 2;
g = g / 2 + target.g() / 2;
b = b / 2 + target.b() / 2;
}
Color32::from_rgba_premultiplied(r, g, b, a)
}

View file

@ -1,266 +0,0 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
linear_u8_from_linear_f32,
};
/// 0-1 linear space `RGBA` color with premultiplied alpha.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Rgba(pub(crate) [f32; 4]);
impl std::ops::Index<usize> for Rgba {
type Output = f32;
#[inline(always)]
fn index(&self, index: usize) -> &f32 {
&self.0[index]
}
}
impl std::ops::IndexMut<usize> for Rgba {
#[inline(always)]
fn index_mut(&mut self, index: usize) -> &mut f32 {
&mut self.0[index]
}
}
#[inline(always)]
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
if f == 0.0 {
state.write_u8(0);
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash;
f.to_bits().hash(state);
}
}
#[allow(clippy::derive_hash_xor_eq)]
impl std::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
crate::f32_hash(state, self.0[0]);
crate::f32_hash(state, self.0[1]);
crate::f32_hash(state, self.0[2]);
crate::f32_hash(state, self.0[3]);
}
}
impl Rgba {
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
#[inline(always)]
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
Self([r, g, b, a])
}
#[inline(always)]
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
Self([r * a, g * a, b * a, a])
}
#[inline(always)]
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r, g, b, a)
}
#[inline(always)]
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
let r = linear_f32_from_gamma_u8(r);
let g = linear_f32_from_gamma_u8(g);
let b = linear_f32_from_gamma_u8(b);
let a = linear_f32_from_linear_u8(a);
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
}
#[inline(always)]
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
Self([r, g, b, 1.0])
}
#[inline(always)]
pub const fn from_gray(l: f32) -> Self {
Self([l, l, l, 1.0])
}
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
crate::ecolor_assert!(0.0 <= l && l <= 1.0);
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
Self([l * a, l * a, l * a, a])
}
/// Transparent black
#[inline(always)]
pub fn from_black_alpha(a: f32) -> Self {
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
Self([0.0, 0.0, 0.0, a])
}
/// Transparent white
#[inline(always)]
pub fn from_white_alpha(a: f32) -> Self {
crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
Self([a, a, a, a])
}
/// Return an additive version of this color (alpha = 0)
#[inline(always)]
pub fn additive(self) -> Self {
let [r, g, b, _] = self.0;
Self([r, g, b, 0.0])
}
/// Multiply with e.g. 0.5 to make us half transparent
#[inline(always)]
pub fn multiply(self, alpha: f32) -> Self {
Self([
alpha * self[0],
alpha * self[1],
alpha * self[2],
alpha * self[3],
])
}
#[inline(always)]
pub fn r(&self) -> f32 {
self.0[0]
}
#[inline(always)]
pub fn g(&self) -> f32 {
self.0[1]
}
#[inline(always)]
pub fn b(&self) -> f32 {
self.0[2]
}
#[inline(always)]
pub fn a(&self) -> f32 {
self.0[3]
}
/// How perceptually intense (bright) is the color?
#[inline]
pub fn intensity(&self) -> f32 {
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
}
/// Returns an opaque version of self
pub fn to_opaque(&self) -> Self {
if self.a() == 0.0 {
// Additive or fully transparent black.
Self::from_rgb(self.r(), self.g(), self.b())
} else {
// un-multiply alpha:
Self::from_rgb(
self.r() / self.a(),
self.g() / self.a(),
self.b() / self.a(),
)
}
}
/// Premultiplied RGBA
#[inline(always)]
pub fn to_array(&self) -> [f32; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
/// Premultiplied RGBA
#[inline(always)]
pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
(self.r(), self.g(), self.b(), self.a())
}
/// unmultiply the alpha
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
let a = self.a();
if a == 0.0 {
// Additive, let's assume we are black
self.0
} else {
[self.r() / a, self.g() / a, self.b() / a, a]
}
}
/// unmultiply the alpha
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
let [r, g, b, a] = self.to_rgba_unmultiplied();
[
gamma_u8_from_linear_f32(r),
gamma_u8_from_linear_f32(g),
gamma_u8_from_linear_f32(b),
linear_u8_from_linear_f32(a.abs()),
]
}
}
impl std::ops::Add for Rgba {
type Output = Rgba;
#[inline(always)]
fn add(self, rhs: Rgba) -> Rgba {
Rgba([
self[0] + rhs[0],
self[1] + rhs[1],
self[2] + rhs[2],
self[3] + rhs[3],
])
}
}
impl std::ops::Mul<Rgba> for Rgba {
type Output = Rgba;
#[inline(always)]
fn mul(self, other: Rgba) -> Rgba {
Rgba([
self[0] * other[0],
self[1] * other[1],
self[2] * other[2],
self[3] * other[3],
])
}
}
impl std::ops::Mul<f32> for Rgba {
type Output = Rgba;
#[inline(always)]
fn mul(self, factor: f32) -> Rgba {
Rgba([
self[0] * factor,
self[1] * factor,
self[2] * factor,
self[3] * factor,
])
}
}
impl std::ops::Mul<Rgba> for f32 {
type Output = Rgba;
#[inline(always)]
fn mul(self, rgba: Rgba) -> Rgba {
Rgba([
self * rgba[0],
self * rgba[1],
self * rgba[2],
self * rgba[3],
])
}
}

View file

@ -1,229 +0,0 @@
# Changelog for eframe
All notable changes to the `eframe` crate.
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
## Unreleased
## 0.21.3 - 2023-02-15
* Fix typing the letter 'P' on web ([#2740](https://github.com/emilk/egui/pull/2740)).
## 0.21.2 - 2023-02-12
* Allow compiling `eframe` with `--no-default-features` ([#2728](https://github.com/emilk/egui/pull/2728)).
## 0.21.1 - 2023-02-12
* Fixed crash when native 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)).
## 0.21.0 - 2023-02-08 - Update to `winit` 0.28
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
#### Desktop/Native:
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Fix bug where the cursor could get stuck using the wrong icon.
* `NativeOptions::transparent` now works with the wgpu backend ([#2684](https://github.com/emilk/egui/pull/2684)).
* Add `Frame::set_minimized` and `set_maximized` ([#2292](https://github.com/emilk/egui/pull/2292), [#2672](https://github.com/emilk/egui/pull/2672)).
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
#### Web:
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/eframe) build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
* Allow empty textures with the glow renderer.
#### Desktop/Native:
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
#### Web:
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* Added `wgpu` rendering backed ([#1564](https://github.com/emilk/egui/pull/1564)):
* Added features `wgpu` and `glow`.
* Added `NativeOptions::renderer` to switch between the rendering backends.
* `egui_glow`: remove calls to `gl.get_error` in release builds to speed up rendering ([#1583](https://github.com/emilk/egui/pull/1583)).
* Added `App::post_rendering` for e.g. reading the framebuffer ([#1591](https://github.com/emilk/egui/pull/1591)).
* Use `Arc` for `glow::Context` instead of `Rc` ([#1640](https://github.com/emilk/egui/pull/1640)).
* Fixed bug where the result returned from `App::on_exit_event` would sometimes be ignored ([#1696](https://github.com/emilk/egui/pull/1696)).
* Added `NativeOptions::follow_system_theme` and `NativeOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
* Selectively expose parts of the API based on target arch (`wasm32` or not) ([#1867](https://github.com/emilk/egui/pull/1867)).
#### Desktop/Native:
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
* Added ability to read window position and size with `frame.info().window_info` ([#1617](https://github.com/emilk/egui/pull/1617)).
* Allow running on native without hardware accelerated rendering. Change with `NativeOptions::hardware_acceleration` ([#1681](https://github.com/emilk/egui/pull/1681), [#1693](https://github.com/emilk/egui/pull/1693)).
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
* Added `Frame::set_visible` ([#1808](https://github.com/emilk/egui/pull/1808)).
* Added fullscreen support ([#1866](https://github.com/emilk/egui/pull/1866)).
* You can now continue execution after closing the native desktop window ([#1889](https://github.com/emilk/egui/pull/1889)).
* `Frame::quit` has been renamed to `Frame::close` and `App::on_exit_event` is now `App::on_close_event` ([#1943](https://github.com/emilk/egui/pull/1943)).
#### Web:
* Added ability to stop/re-run web app from JavaScript. ⚠️ You need to update your CSS with `html, body: { height: 100%; width: 100%; }` ([#1803](https://github.com/emilk/egui/pull/1650)).
* Added `WebOptions::follow_system_theme` and `WebOptions::default_theme` ([#1726](https://github.com/emilk/egui/pull/1726)).
* Added option to select WebGL version ([#1803](https://github.com/emilk/egui/pull/1803)).
## 0.18.0 - 2022-04-30
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Removed `eframe::epi` - everything is now in `eframe` (`eframe::App`, `eframe::Frame` etc) ([#1545](https://github.com/emilk/egui/pull/1545)).
* Removed `Frame::request_repaint` - just call `egui::Context::request_repaint` for the same effect ([#1366](https://github.com/emilk/egui/pull/1366)).
* Changed app creation/setup ([#1363](https://github.com/emilk/egui/pull/1363)):
* Removed `App::setup` and `App::name`.
* Provide `CreationContext` when creating app with egui context, storage, integration info and glow context.
* Change interface of `run_native` and `start_web`.
* Added `Frame::storage()` and `Frame::storage_mut()` ([#1418](https://github.com/emilk/egui/pull/1418)).
* You can now load/save state in `App::update`
* Changed `App::update` to take `&mut Frame` instead of `&Frame`.
* `Frame` is no longer `Clone` or `Sync`.
* Added `glow` (OpenGL) context to `Frame` ([#1425](https://github.com/emilk/egui/pull/1425)).
#### Desktop/Native:
* Remove the `egui_glium` feature. `eframe` will now always use `egui_glow` as the native backend ([#1357](https://github.com/emilk/egui/pull/1357)).
* Change default for `NativeOptions::drag_and_drop_support` to `true` ([#1329](https://github.com/emilk/egui/pull/1329)).
* Added new `NativeOptions`: `vsync`, `multisampling`, `depth_buffer`, `stencil_buffer`.
* `dark-light` (dark mode detection) is now an opt-in feature ([#1437](https://github.com/emilk/egui/pull/1437)).
* 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)).
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
* Moved app persistence to a background thread, allowing for smoother frame rates (on native).
* Added `Frame::set_window_pos` ([#1505](https://github.com/emilk/egui/pull/1505)).
#### Web:
* Use full browser width by default ([#1378](https://github.com/emilk/egui/pull/1378)).
* egui code will no longer be called after panic ([#1306](https://github.com/emilk/egui/pull/1306)).
## 0.17.0 - 2022-02-22
* Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)).
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)).
#### Desktop/Native:
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Fixed horizontal scrolling direction on Linux.
* Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038))
* Added `NativeOptions::initial_window_pos`.
* Fixed `enable_drag` for Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
#### Web:
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).
* Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)).
* Updated `eframe::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)).
* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)).
## 0.16.0 - 2021-12-29
* `Frame` can now be cloned, saved, and passed to background threads ([#999](https://github.com/emilk/egui/pull/999)).
* Added `Frame::request_repaint` to replace `repaint_signal` ([#999](https://github.com/emilk/egui/pull/999)).
* Added `Frame::alloc_texture/free_texture` to replace `tex_allocator` ([#999](https://github.com/emilk/egui/pull/999)).
#### Web:
* Fixed [dark rendering in WebKitGTK](https://github.com/emilk/egui/issues/794) ([#888](https://github.com/emilk/egui/pull/888/)).
* Added feature `glow` to switch to a [`glow`](https://github.com/grovesNL/glow) based painter ([#868](https://github.com/emilk/egui/pull/868)).
## 0.15.0 - 2021-10-24
* `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
* `Frame` now provides `set_decorations` to set whether to show window decorations.
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
#### Desktop/Native:
* Increase native scroll speed.
* Added new backend `egui_glow` as an alternative to `egui_glium`. Enable with `default-features = false, features = ["default_fonts", "egui_glow"]`.
#### Web:
* Implement `eframe::NativeTexture` trait for the WebGL painter.
* Deprecate `Painter::register_webgl_texture.
* Fixed multiline paste.
* Fixed painting with non-opaque backgrounds.
* Improve text input on mobile and for IME.
## 0.14.0 - 2021-08-24
* Added dragging and dropping files into egui.
* Improve http fetch API.
* `run_native` now returns when the app is closed.
* Web: Made text thicker and less pixelated.
## 0.13.1 - 2021-06-24
* Fixed `http` feature flag and docs
## 0.13.0 - 2021-06-24
* `App::setup` now takes a `Frame` and `Storage` by argument.
* `App::load` has been removed. Implement `App::setup` instead.
* Web: Default to light visuals unless the system reports a preference for dark mode.
* Web: Improve alpha blending, making fonts look much better (especially in light mode)
* Web: Fix double-paste bug
## 0.12.0 - 2021-05-10
* Moved options out of `trait App` into new `NativeOptions`.
* Added option for `always_on_top`.
* Web: Scroll faster when scrolling with mouse wheel.
## 0.11.0 - 2021-04-05
* You can now turn your window transparent with the `App::transparent` option.
* You can now disable window decorations with the `App::decorated` option.
* Web: [Fix mobile and IME text input](https://github.com/emilk/egui/pull/253)
* Web: Hold down a modifier key when clicking a link to open it in a new tab.
Contributors: [n2](https://github.com/n2)
## 0.10.0 - 2021-02-28
* [You can now set your own app icons](https://github.com/emilk/egui/pull/193).
* You can control the initial size of the native window with `App::initial_window_size`.
* You can control the maximum egui web canvas size with `App::max_size_points`.
* `Frame::tex_allocator()` no longer returns an `Option` (there is always a texture allocator).
## 0.9.0 - 2021-02-07
* [Added support for HTTP body](https://github.com/emilk/egui/pull/139).
* Web: Right-clicks will no longer open browser context menu.
* Web: Fix a bug where one couldn't select items in a combo box on a touch screen.
## 0.8.0 - 2021-01-17
* Simplify `TextureAllocator` interface.
* WebGL2 is now supported, with improved texture sampler. WebGL1 will be used as a fallback.
* Web: Slightly improved alpha-blending (work-around for non-existing linear-space blending).
* Web: Call `prevent_default` for arrow keys when entering text
## 0.7.0 - 2021-01-04
* Initial release of `eframe`

View file

@ -1,169 +0,0 @@
[package]
name = "eframe"
version = "0.21.3"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[lib]
[features]
default = ["accesskit", "default_fonts", "glow"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
##
## See also [`NativeOptions::follow_system_theme`] and [`NativeOptions::default_theme`].
dark-light = ["dep:dark-light"]
## If set, egui will use `include_bytes!` to bundle some fonts.
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
## Enable saving app state to disk.
persistence = [
"directories-next",
"egui-winit/serde",
"egui/persistence",
"ron",
"serde",
]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers.
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
##
## For other platforms, use the "accesskit" feature instead.
web_screen_reader = ["tts"]
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
## This is used to generate images for the examples.
__screenshot = ["dep:image"]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## This overrides the `glow` feature.
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
[dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"bytemuck",
"tracing",
] }
thiserror = "1.0.37"
tracing = { version = "0.1", default-features = false, features = ["std"] }
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
glow = { version = "0.12", optional = true }
ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] }
# -------------------------------------------
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
"clipboard",
"links",
] }
raw-window-handle = { version = "0.5.0" }
winit = "0.28.1"
# optional native:
dark-light = { version = "1.0", optional = true }
directories-next = { version = "2", optional = true }
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
"winit",
] } # if wgpu is used, use it with winit
pollster = { version = "0.3", optional = true } # needed for wgpu
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
# this can be done at the same time we expose x11/wayland features of winit crate.
glutin = { version = "0.30", optional = true }
glutin-winit = { version = "0.3.0", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [
"png",
] }
puffin = { version = "0.14", optional = true }
wgpu = { version = "0.15.0", optional = true }
# -------------------------------------------
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck = "1.7"
js-sys = "0.3"
percent-encoding = "2.1"
wasm-bindgen = "=0.2.84"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.58", features = [
"BinaryType",
"Blob",
"Clipboard",
"ClipboardEvent",
"CompositionEvent",
"console",
"CssStyleDeclaration",
"DataTransfer",
"DataTransferItem",
"DataTransferItemList",
"Document",
"DomRect",
"DragEvent",
"Element",
"Event",
"EventListener",
"EventTarget",
"ExtSRgb",
"File",
"FileList",
"FocusEvent",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",
"InputEvent",
"KeyboardEvent",
"Location",
"MediaQueryList",
"MouseEvent",
"Navigator",
"Performance",
"Storage",
"Touch",
"TouchEvent",
"TouchList",
"WebGl2RenderingContext",
"WebglDebugRendererInfo",
"WebGlRenderingContext",
"WheelEvent",
"Window",
] }
# optional web:
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
tts = { version = "0.25", optional = true, default-features = false }
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }

View file

@ -1,64 +0,0 @@
# eframe: the [`egui`](https://github.com/emilk/egui) framework
[![Latest version](https://img.shields.io/crates/v/eframe.svg)](https://crates.io/crates/eframe)
[![Documentation](https://docs.rs/eframe/badge.svg)](https://docs.rs/eframe)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
`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).
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
For how to use `egui`, see [the egui docs](https://docs.rs/egui).
---
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
To use on Linux, first run:
```
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
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.
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
## Alternatives
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
## Problems with running egui on the web
`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.
* Rendering: Getting pixel-perfect rendering right on the web is very difficult.
* Search: you cannot search an egui web page like you would a normal web page.
* 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.
* Mobile text editing is not as good as for a normal web app.
* 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).
* No integration with browser settings for colors and fonts.
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).
The suggested use for `eframe` are for web apps where performance and responsiveness are more important than accessibility and mobile text editing.
## Companion crates
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:
* Audio: [`cpal`](https://github.com/RustAudio/cpal).
* HTTP client: [`ehttp`](https://github.com/emilk/ehttp) and [`reqwest`](https://github.com/seanmonstar/reqwest).
* Time: [`chrono`](https://github.com/chronotope/chrono).
* WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock).
## Name
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).

File diff suppressed because it is too large Load diff

View file

@ -1,262 +0,0 @@
//! eframe - the [`egui`] framework crate
//!
//! If you are planning to write an app for web or native,
//! and want to use [`egui`] for everything, then `eframe` is for you!
//!
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//!
//! In short, you implement [`App`] (especially [`App::update`]) and then
//! call [`crate::run_native`] from your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`.
//!
//! ## Usage, native:
//! ``` no_run
//! use eframe::egui;
//!
//! fn main() {
//! let native_options = eframe::NativeOptions::default();
//! eframe::run_native("My egui App", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
//! }
//!
//! #[derive(Default)]
//! struct MyEguiApp {}
//!
//! impl MyEguiApp {
//! fn new(cc: &eframe::CreationContext<'_>) -> Self {
//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
//! // Restore app state using cc.storage (requires the "persistence" feature).
//! // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
//! // for e.g. egui::PaintCallback.
//! Self::default()
//! }
//! }
//!
//! impl eframe::App for MyEguiApp {
//! fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
//! egui::CentralPanel::default().show(ctx, |ui| {
//! ui.heading("Hello World!");
//! });
//! }
//! }
//! ```
//!
//! ## Usage, web:
//! ``` no_run
//! #[cfg(target_arch = "wasm32")]
//! use wasm_bindgen::prelude::*;
//!
//! /// Call this once from the HTML.
//! #[cfg(target_arch = "wasm32")]
//! #[wasm_bindgen]
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
//! let web_options = eframe::WebOptions::default();
//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
//! }
//! ```
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::needless_doctest_main)]
// Re-export all useful libraries:
pub use {egui, egui::emath, egui::epaint};
#[cfg(feature = "glow")]
pub use {egui_glow, glow};
#[cfg(feature = "wgpu")]
pub use {egui_wgpu, wgpu};
mod epi;
// Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is:
pub use epi::*;
// ----------------------------------------------------------------------------
// When compiling for web
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen;
#[cfg(target_arch = "wasm32")]
use web::AppRunnerRef;
#[cfg(target_arch = "wasm32")]
pub use web_sys;
/// Install event listeners to register different input events
/// and start running the given app.
///
/// ``` no_run
/// #[cfg(target_arch = "wasm32")]
/// use wasm_bindgen::prelude::*;
///
/// /// This is the entry-point for all the web-assembly.
/// /// This is called from the HTML.
/// /// It loads the app, installs some callbacks, then returns.
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
/// /// You can add more callbacks like this if you want to call in to your code.
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub struct WebHandle {
/// handle: AppRunnerRef,
/// }
/// #[cfg(target_arch = "wasm32")]
/// #[wasm_bindgen]
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
/// let web_options = eframe::WebOptions::default();
/// eframe::start_web(
/// canvas_id,
/// web_options,
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
/// )
/// .await
/// .map(|handle| WebHandle { handle })
/// }
/// ```
///
/// # Errors
/// Failing to initialize WebGL graphics.
#[cfg(target_arch = "wasm32")]
pub async fn start_web(
canvas_id: &str,
web_options: WebOptions,
app_creator: AppCreator,
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator).await?;
Ok(handle)
}
// ----------------------------------------------------------------------------
// When compiling natively
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod native;
/// This is how you start a native (desktop) app.
///
/// The first argument is name of your app, used for the title bar of the native window
/// and the save location of persistence (see [`App::save`]).
///
/// Call from `fn main` like this:
/// ``` no_run
/// use eframe::egui;
///
/// fn main() {
/// let native_options = eframe::NativeOptions::default();
/// eframe::run_native("MyApp", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
/// }
///
/// #[derive(Default)]
/// struct MyEguiApp {}
///
/// impl MyEguiApp {
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
/// // Restore app state using cc.storage (requires the "persistence" feature).
/// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
/// // for e.g. egui::PaintCallback.
/// Self::default()
/// }
/// }
///
/// impl eframe::App for MyEguiApp {
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
/// }
/// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub fn run_native(
app_name: &str,
native_options: NativeOptions,
app_creator: AppCreator,
) -> Result<()> {
let renderer = native_options.renderer;
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
tracing::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
}
// ----------------------------------------------------------------------------
/// The different problems that can occur when trying to run `eframe`.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(not(target_arch = "wasm32"))]
#[error("winit error: {0}")]
Winit(#[from] winit::error::OsError),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("glutin error: {0}")]
Glutin(#[from] glutin::error::Error),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
#[cfg(feature = "wgpu")]
#[error("WGPU error: {0}")]
Wgpu(#[from] egui_wgpu::WgpuError),
}
pub type Result<T> = std::result::Result<T, Error>;
// ---------------------------------------------------------------------------
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) use profiling_scopes::*;

View file

@ -1,567 +0,0 @@
use winit::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
use winit::platform::macos::WindowBuilderExtMacOS as _;
#[cfg(feature = "accesskit")]
use egui::accesskit;
use egui::NumExt as _;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
use crate::{epi, Theme, WindowInfo};
#[derive(Default)]
pub struct WindowState {
// We cannot simply call `winit::Window::is_minimized/is_maximized`
// because that deadlocks on mac.
pub minimized: bool,
pub maximized: bool,
}
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize {
width: points.x as f64,
height: points.y as f64,
}
}
pub fn read_window_info(
window: &winit::window::Window,
pixels_per_point: f32,
window_state: &WindowState,
) -> WindowInfo {
let position = window
.outer_position()
.ok()
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
let monitor = window.current_monitor().is_some();
let monitor_size = if monitor {
let size = window.current_monitor().unwrap().size();
Some(egui::vec2(size.width as _, size.height as _))
} else {
None
};
let size = window
.inner_size()
.to_logical::<f32>(pixels_per_point.into());
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
WindowInfo {
position,
fullscreen: window.fullscreen().is_some(),
minimized: window_state.minimized,
maximized: window_state.maximized,
size: egui::Vec2 {
x: size.width,
y: size.height,
},
monitor_size,
}
}
pub fn window_builder<E>(
event_loop: &EventLoopWindowTarget<E>,
title: &str,
native_options: &epi::NativeOptions,
window_settings: Option<WindowSettings>,
) -> winit::window::WindowBuilder {
let epi::NativeOptions {
maximized,
decorated,
fullscreen,
#[cfg(target_os = "macos")]
fullsize_content,
drag_and_drop_support,
icon_data,
initial_window_pos,
initial_window_size,
min_window_size,
max_window_size,
resizable,
transparent,
centered,
..
} = native_options;
let window_icon = icon_data.clone().and_then(load_icon);
let mut window_builder = winit::window::WindowBuilder::new()
.with_title(title)
.with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
.with_maximized(*maximized)
.with_resizable(*resizable)
.with_transparent(*transparent)
.with_window_icon(window_icon)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
#[cfg(target_os = "macos")]
if *fullsize_content {
window_builder = window_builder
.with_title_hidden(true)
.with_titlebar_transparent(true)
.with_fullsize_content_view(true);
}
if let Some(min_size) = *min_window_size {
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
}
if let Some(max_size) = *max_window_size {
window_builder = window_builder.with_max_inner_size(points_to_size(max_size));
}
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
let inner_size_points = if let Some(mut window_settings) = window_settings {
// Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)]
window_settings.clamp_window_to_sane_position(&event_loop);
window_builder = window_settings.initialize_window(window_builder);
window_settings.inner_size_points()
} else {
if let Some(pos) = *initial_window_pos {
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
if let Some(initial_window_size) = *initial_window_size {
let initial_window_size =
initial_window_size.at_most(largest_monitor_point_size(event_loop));
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
}
*initial_window_size
};
if *centered {
if let Some(monitor) = event_loop.available_monitors().next() {
let monitor_size = monitor.size();
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0 && monitor_size.height > 0 {
let x = (monitor_size.width - inner_size.x as u32) / 2;
let y = (monitor_size.height - inner_size.y as u32) / 2;
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
x: x as f64,
y: y as f64,
});
}
}
}
window_builder
}
pub fn apply_native_options_to_window(
window: &winit::window::Window,
native_options: &crate::NativeOptions,
) {
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
}
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
let mut max_size = egui::Vec2::ZERO;
for monitor in event_loop.available_monitors() {
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
let size = egui::vec2(size.width, size.height);
max_size = max_size.max(size);
}
if max_size == egui::Vec2::ZERO {
egui::Vec2::splat(16000.0)
} else {
max_size
}
}
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
}
#[cfg(target_os = "windows")]
fn window_builder_drag_and_drop(
window_builder: winit::window::WindowBuilder,
enable: bool,
) -> winit::window::WindowBuilder {
use winit::platform::windows::WindowBuilderExtWindows as _;
window_builder.with_drag_and_drop(enable)
}
#[cfg(not(target_os = "windows"))]
fn window_builder_drag_and_drop(
window_builder: winit::window::WindowBuilder,
_enable: bool,
) -> winit::window::WindowBuilder {
// drag and drop can only be disabled on windows
window_builder
}
pub fn handle_app_output(
window: &winit::window::Window,
current_pixels_per_point: f32,
app_output: epi::backend::AppOutput,
window_state: &mut WindowState,
) {
let epi::backend::AppOutput {
close: _,
window_size,
window_title,
decorated,
fullscreen,
drag_window,
window_pos,
visible: _, // handled in post_present
always_on_top,
minimized,
maximized,
} = app_output;
if let Some(decorated) = decorated {
window.set_decorations(decorated);
}
if let Some(window_size) = window_size {
window.set_inner_size(
winit::dpi::PhysicalSize {
width: (current_pixels_per_point * window_size.x).round(),
height: (current_pixels_per_point * window_size.y).round(),
}
.to_logical::<f32>(native_pixels_per_point(window) as f64),
);
}
if let Some(fullscreen) = fullscreen {
window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
}
if let Some(window_title) = window_title {
window.set_title(&window_title);
}
if let Some(window_pos) = window_pos {
window.set_outer_position(winit::dpi::PhysicalPosition {
x: window_pos.x as f64,
y: window_pos.y as f64,
});
}
if drag_window {
let _ = window.drag_window();
}
if let Some(always_on_top) = always_on_top {
use winit::window::WindowLevel;
window.set_window_level(if always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});
}
if let Some(minimized) = minimized {
window.set_minimized(minimized);
window_state.minimized = minimized;
}
if let Some(maximized) = maximized {
window.set_maximized(maximized);
window_state.maximized = maximized;
}
}
// ----------------------------------------------------------------------------
/// For loading/saving app state and/or egui memory to disk.
pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
#[cfg(feature = "persistence")]
if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) {
return Some(Box::new(storage));
}
None
}
// ----------------------------------------------------------------------------
/// Everything needed to make a winit-based integration for [`epi`].
pub struct EpiIntegration {
pub frame: epi::Frame,
last_auto_save: std::time::Instant,
pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput,
egui_winit: egui_winit::State,
/// When set, it is time to close the native window.
close: bool,
can_drag_window: bool,
window_state: WindowState,
}
impl EpiIntegration {
pub fn new<E>(
event_loop: &EventLoopWindowTarget<E>,
max_texture_side: usize,
window: &winit::window::Window,
system_theme: Option<Theme>,
storage: Option<Box<dyn epi::Storage>>,
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
) -> Self {
let egui_ctx = egui::Context::default();
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
egui_ctx.memory_mut(|mem| *mem = memory);
let native_pixels_per_point = window.scale_factor() as f32;
let window_state = WindowState {
minimized: window.is_minimized().unwrap_or(false),
maximized: window.is_maximized(),
};
let frame = epi::Frame {
info: epi::IntegrationInfo {
system_theme,
cpu_usage: None,
native_pixels_per_point: Some(native_pixels_per_point),
window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state),
},
output: epi::backend::AppOutput {
visible: Some(true),
..Default::default()
},
storage,
#[cfg(feature = "glow")]
gl,
#[cfg(feature = "wgpu")]
wgpu_render_state,
};
let mut egui_winit = egui_winit::State::new(event_loop);
egui_winit.set_max_texture_side(max_texture_side);
egui_winit.set_pixels_per_point(native_pixels_per_point);
Self {
frame,
last_auto_save: std::time::Instant::now(),
egui_ctx,
egui_winit,
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
window_state,
}
}
#[cfg(feature = "accesskit")]
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
) {
let egui_ctx = self.egui_ctx.clone();
self.egui_winit
.init_accesskit(window, event_loop_proxy, move || {
// This function is called when an accessibility client
// (e.g. screen reader) makes its first request. If we got here,
// we know that an accessibility tree is actually wanted.
egui_ctx.enable_accesskit();
// Enqueue a repaint so we'll receive a full tree update soon.
egui_ctx.request_repaint();
egui_ctx.accesskit_placeholder_tree_update()
});
}
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
self.egui_ctx
.memory_mut(|mem| mem.set_everything_is_visible(true));
let full_output = self.update(app, window);
self.pending_full_output.append(full_output); // Handle it next frame
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
/// If `true`, it is time to close the native window.
pub fn should_close(&self) -> bool {
self.close
}
pub fn on_event(
&mut self,
app: &mut dyn epi::App,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
WindowEvent::CloseRequested => {
tracing::debug!("Received WindowEvent::CloseRequested");
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
WindowEvent::Destroyed => {
tracing::debug!("Received WindowEvent::Destroyed");
self.close = true;
}
WindowEvent::MouseInput {
button: MouseButton::Left,
state: ElementState::Pressed,
..
} => self.can_drag_window = true,
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.frame.info.native_pixels_per_point = Some(*scale_factor as _);
}
_ => {}
}
self.egui_winit.on_event(&self.egui_ctx, event)
}
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_winit.on_accesskit_action_request(request);
}
pub fn update(
&mut self,
app: &mut dyn epi::App,
window: &winit::window::Window,
) -> egui::FullOutput {
let frame_start = std::time::Instant::now();
self.frame.info.window_info =
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
let raw_input = self.egui_winit.take_egui_input(window);
// Run user code:
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
crate::profile_scope!("App::update");
app.update(egui_ctx, &mut self.frame);
});
self.pending_full_output.append(full_output);
let full_output = std::mem::take(&mut self.pending_full_output);
{
let mut app_output = self.frame.take_app_output();
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
self.can_drag_window = false;
if app_output.close {
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
self.frame.output.visible = app_output.visible; // this is handled by post_present
handle_app_output(
window,
self.egui_ctx.pixels_per_point(),
app_output,
&mut self.window_state,
);
}
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
self.frame.info.cpu_usage = Some(frame_time);
full_output
}
pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
let inner_size = window.inner_size();
let window_size_px = [inner_size.width, inner_size.height];
app.post_rendering(window_size_px, &self.frame);
}
pub fn post_present(&mut self, window: &winit::window::Window) {
if let Some(visible) = self.frame.output.visible.take() {
window.set_visible(visible);
}
}
pub fn handle_platform_output(
&mut self,
window: &winit::window::Window,
platform_output: egui::PlatformOutput,
) {
self.egui_winit
.handle_platform_output(window, &self.egui_ctx, platform_output);
}
// ------------------------------------------------------------------------
// Persistance stuff:
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
let now = std::time::Instant::now();
if now - self.last_auto_save > app.auto_save_interval() {
self.save(app, window);
self.last_auto_save = now;
}
}
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) {
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {
crate::profile_function!();
if _app.persist_native_window() {
crate::profile_scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_display(_window),
);
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
crate::profile_scope!("App::save");
_app.save(storage);
}
crate::profile_scope!("Storage::flush");
storage.flush();
}
}
}
#[cfg(feature = "persistence")]
const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
#[cfg(feature = "persistence")]
const STORAGE_WINDOW_KEY: &str = "window";
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
}
#[cfg(not(feature = "persistence"))]
None
}
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
}
#[cfg(not(feature = "persistence"))]
None
}

View file

@ -1,117 +0,0 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
// ----------------------------------------------------------------------------
/// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk.
/// Used to restore egui state, glium window position/size and app state.
pub struct FileStorage {
ron_filepath: PathBuf,
kv: HashMap<String, String>,
dirty: bool,
last_save_join_handle: Option<std::thread::JoinHandle<()>>,
}
impl Drop for FileStorage {
fn drop(&mut self) {
if let Some(join_handle) = self.last_save_join_handle.take() {
join_handle.join().ok();
}
}
}
impl FileStorage {
/// Store the state in this .ron file.
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
let ron_filepath: PathBuf = ron_filepath.into();
tracing::debug!("Loading app state from {:?}…", ron_filepath);
Self {
kv: read_ron(&ron_filepath).unwrap_or_default(),
ron_filepath,
dirty: false,
last_save_join_handle: None,
}
}
/// Find a good place to put the files that the OS likes.
pub fn from_app_name(app_name: &str) -> Option<Self> {
if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) {
let data_dir = proj_dirs.data_dir().to_path_buf();
if let Err(err) = std::fs::create_dir_all(&data_dir) {
tracing::warn!(
"Saving disabled: Failed to create app path at {:?}: {}",
data_dir,
err
);
None
} else {
Some(Self::from_ron_filepath(data_dir.join("app.ron")))
}
} else {
tracing::warn!("Saving disabled: Failed to find path to data_dir.");
None
}
}
}
impl crate::Storage for FileStorage {
fn get_string(&self, key: &str) -> Option<String> {
self.kv.get(key).cloned()
}
fn set_string(&mut self, key: &str, value: String) {
if self.kv.get(key) != Some(&value) {
self.kv.insert(key.to_owned(), value);
self.dirty = true;
}
}
fn flush(&mut self) {
if self.dirty {
self.dirty = false;
let file_path = self.ron_filepath.clone();
let kv = self.kv.clone();
if let Some(join_handle) = self.last_save_join_handle.take() {
// wait for previous save to complete.
join_handle.join().ok();
}
let join_handle = std::thread::spawn(move || {
let file = std::fs::File::create(&file_path).unwrap();
let config = Default::default();
ron::ser::to_writer_pretty(file, &kv, config).unwrap();
tracing::trace!("Persisted to {:?}", file_path);
});
self.last_save_join_handle = Some(join_handle);
}
}
}
// ----------------------------------------------------------------------------
fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
match std::fs::File::open(ron_path) {
Ok(file) => {
let reader = std::io::BufReader::new(file);
match ron::de::from_reader(reader) {
Ok(value) => Some(value),
Err(err) => {
tracing::warn!("Failed to parse RON: {}", err);
None
}
}
}
Err(_err) => {
// File probably doesn't exist. That's fine.
None
}
}
}

View file

@ -1,6 +0,0 @@
mod epi_integration;
pub mod run;
/// File storage which can be used by native backends.
#[cfg(feature = "persistence")]
pub mod file_storage;

File diff suppressed because it is too large Load diff

View file

@ -1,585 +0,0 @@
use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
use crate::{epi, App};
use super::{web_painter::WebPainter, *};
// ----------------------------------------------------------------------------
/// Data gathered between frames.
#[derive(Default)]
pub struct WebInput {
/// Required because we don't get a position on touched
pub latest_touch_pos: Option<egui::Pos2>,
/// Required to maintain a stable touch position for multi-touch gestures.
pub latest_touch_pos_id: Option<egui::TouchId>,
pub raw: egui::RawInput,
}
impl WebInput {
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
pixels_per_point: Some(native_pixels_per_point()), // We ALWAYS use the native pixels-per-point
time: Some(now_sec()),
..self.raw.take()
}
}
}
// ----------------------------------------------------------------------------
use std::sync::atomic::Ordering::SeqCst;
/// Stores when to do the next repaint.
pub struct NeedRepaint(Mutex<f64>);
impl Default for NeedRepaint {
fn default() -> Self {
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
}
}
impl NeedRepaint {
/// Returns the time (in [`now_sec`] scale) when
/// we should next repaint.
pub fn when_to_repaint(&self) -> f64 {
*self.0.lock()
}
/// Unschedule repainting.
pub fn clear(&self) {
*self.0.lock() = f64::INFINITY;
}
pub fn repaint_after(&self, num_seconds: f64) {
let mut repaint_time = self.0.lock();
*repaint_time = repaint_time.min(now_sec() + num_seconds);
}
pub fn repaint_asap(&self) {
*self.0.lock() = f64::NEG_INFINITY;
}
}
pub struct IsDestroyed(std::sync::atomic::AtomicBool);
impl Default for IsDestroyed {
fn default() -> Self {
Self(false.into())
}
}
impl IsDestroyed {
pub fn fetch(&self) -> bool {
self.0.load(SeqCst)
}
pub fn set_true(&self) {
self.0.store(true, SeqCst);
}
}
// ----------------------------------------------------------------------------
fn user_agent() -> Option<String> {
web_sys::window()?.navigator().user_agent().ok()
}
fn web_location() -> epi::Location {
let location = web_sys::window().unwrap().location();
let hash = percent_decode(&location.hash().unwrap_or_default());
let query = location
.search()
.unwrap_or_default()
.strip_prefix('?')
.map(percent_decode)
.unwrap_or_default();
let query_map = parse_query_map(&query)
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect();
epi::Location {
url: percent_decode(&location.href().unwrap_or_default()),
protocol: percent_decode(&location.protocol().unwrap_or_default()),
host: percent_decode(&location.host().unwrap_or_default()),
hostname: percent_decode(&location.hostname().unwrap_or_default()),
port: percent_decode(&location.port().unwrap_or_default()),
hash,
query,
query_map,
origin: percent_decode(&location.origin().unwrap_or_default()),
}
}
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
query
.split('&')
.filter_map(|pair| {
if pair.is_empty() {
None
} else {
Some(if let Some((key, value)) = pair.split_once('=') {
(key, value)
} else {
(pair, "")
})
}
})
.collect()
}
#[test]
fn test_parse_query() {
assert_eq!(parse_query_map(""), BTreeMap::default());
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
assert_eq!(
parse_query_map("foo=bar"),
BTreeMap::from_iter([("foo", "bar")])
);
assert_eq!(
parse_query_map("foo=bar&baz=42"),
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz=42"),
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz&&"),
BTreeMap::from_iter([("foo", ""), ("baz", "")])
);
}
// ----------------------------------------------------------------------------
pub struct AppRunner {
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
painter: ActiveWebPainter,
pub(crate) input: WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
last_save_time: f64,
screen_reader: super::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta,
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
}
impl Drop for AppRunner {
fn drop(&mut self) {
tracing::debug!("AppRunner has fully dropped");
}
}
impl AppRunner {
/// # Errors
/// Failure to initialize WebGL renderer.
pub async fn new(
canvas_id: &str,
web_options: crate::WebOptions,
app_creator: epi::AppCreator,
) -> Result<Self, String> {
let painter = ActiveWebPainter::new(canvas_id, &web_options).await?;
let system_theme = if web_options.follow_system_theme {
super::system_theme()
} else {
None
};
let info = epi::IntegrationInfo {
web_info: epi::WebInfo {
user_agent: user_agent().unwrap_or_default(),
location: web_location(),
},
system_theme,
cpu_usage: None,
native_pixels_per_point: Some(native_pixels_per_point()),
};
let storage = LocalStorage::default();
let egui_ctx = egui::Context::default();
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
&user_agent().unwrap_or_default(),
));
load_memory(&egui_ctx);
let theme = system_theme.unwrap_or(web_options.default_theme);
egui_ctx.set_visuals(theme.egui_visuals());
let app = app_creator(&epi::CreationContext {
egui_ctx: egui_ctx.clone(),
integration_info: info.clone(),
storage: Some(&storage),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
});
let frame = epi::Frame {
info,
output: Default::default(),
storage: Some(Box::new(storage)),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
{
let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move || {
needs_repaint.repaint_asap();
});
}
let mut runner = Self {
frame,
egui_ctx,
painter,
input: Default::default(),
app,
needs_repaint,
is_destroyed: Default::default(),
last_save_time: now_sec(),
screen_reader: Default::default(),
text_cursor_pos: None,
mutable_text_under_cursor: false,
textures_delta: Default::default(),
events_to_unsubscribe: Default::default(),
};
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
Ok(runner)
}
pub fn egui_ctx(&self) -> &egui::Context {
&self.egui_ctx
}
/// Get mutable access to the concrete [`App`] we enclose.
///
/// This will panic if your app does not implement [`App::as_any_mut`].
pub fn app_mut<ConreteApp: 'static + App>(&mut self) -> &mut ConreteApp {
self.app
.as_any_mut()
.expect("Your app must implement `as_any_mut`, but it doesn't")
.downcast_mut::<ConreteApp>()
.unwrap()
}
pub fn auto_save(&mut self) {
let now = now_sec();
let time_since_last_save = now - self.last_save_time;
if time_since_last_save > self.app.auto_save_interval().as_secs_f64() {
if self.app.persist_egui_memory() {
save_memory(&self.egui_ctx);
}
if let Some(storage) = self.frame.storage_mut() {
self.app.save(storage);
}
self.last_save_time = now;
}
}
pub fn canvas_id(&self) -> &str {
self.painter.canvas_id()
}
pub fn warm_up(&mut self) -> Result<(), JsValue> {
if self.app.warm_up_enabled() {
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
self.egui_ctx
.memory_mut(|m| m.set_everything_is_visible(true));
self.logic()?;
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
Ok(())
}
pub fn destroy(&mut self) -> Result<(), JsValue> {
let is_destroyed_already = self.is_destroyed.fetch();
if is_destroyed_already {
tracing::warn!("App was destroyed already");
Ok(())
} else {
tracing::debug!("Destroying");
for x in self.events_to_unsubscribe.drain(..) {
x.unsubscribe()?;
}
self.painter.destroy();
self.is_destroyed.set_true();
Ok(())
}
}
/// Returns how long to wait until the next repaint.
///
/// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> Result<(std::time::Duration, Vec<egui::ClippedPrimitive>), JsValue> {
let frame_start = now_sec();
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
let canvas_size = canvas_size_in_points(self.canvas_id());
let raw_input = self.input.new_frame(canvas_size);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
self.app.update(egui_ctx, &mut self.frame);
});
let egui::FullOutput {
platform_output,
repaint_after,
textures_delta,
shapes,
} = full_output;
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
let clipped_primitives = self.egui_ctx.tessellate(shapes);
{
let app_output = self.frame.take_app_output();
let epi::backend::AppOutput {} = app_output;
}
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
Ok((repaint_after, clipped_primitives))
}
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
let textures_delta = std::mem::take(&mut self.textures_delta);
self.painter.paint_and_update_textures(
self.app.clear_color(&self.egui_ctx.style().visuals),
clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
)?;
Ok(())
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options(|o| o.screen_reader) {
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::PlatformOutput {
cursor_icon,
open_url,
copied_text,
events: _, // already handled
mutable_text_under_cursor,
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update: _, // not currently implemented
} = platform_output;
set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
super::open_url(&open.url, open.new_tab);
}
#[cfg(web_sys_unstable_apis)]
if !copied_text.is_empty() {
set_clipboard_text(&copied_text);
}
#[cfg(not(web_sys_unstable_apis))]
let _ = copied_text;
self.mutable_text_under_cursor = mutable_text_under_cursor;
if self.text_cursor_pos != text_cursor_pos {
text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
self.text_cursor_pos = text_cursor_pos;
}
}
}
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
pub struct TargetEvent {
target: EventTarget,
event_name: String,
closure: Closure<dyn FnMut(web_sys::Event)>,
}
pub struct IntervalHandle {
pub handle: i32,
pub closure: Closure<dyn FnMut()>,
}
pub enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(dead_code)]
IntervalHandle(IntervalHandle),
}
impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> {
match self {
EventToUnsubscribe::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback(
handle.event_name.as_str(),
handle.closure.as_ref().unchecked_ref(),
)?;
Ok(())
}
EventToUnsubscribe::IntervalHandle(handle) => {
let window = web_sys::window().unwrap();
window.clear_interval_with_handle(handle.handle);
Ok(())
}
}
}
}
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
/// Set to `true` if there is a panic.
/// Used to ignore callbacks after a panic.
pub panicked: Arc<AtomicBool>,
pub events: Vec<EventToUnsubscribe>,
}
impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&mut self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.runner.clone();
let panicked = self.panicked.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if !panicked.load(Ordering::SeqCst) {
// Cast the event to the expected event type
let event = event.unchecked_into::<E>();
closure(event, runner_ref.lock());
}
}) as Box<dyn FnMut(web_sys::Event)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
closure,
};
self.events.push(EventToUnsubscribe::TargetEvent(handle));
Ok(())
}
}
// ----------------------------------------------------------------------------
/// Install event listeners to register different input events
/// and start running the given app.
pub async fn start(
canvas_id: &str,
web_options: crate::WebOptions,
app_creator: epi::AppCreator,
) -> Result<AppRunnerRef, JsValue> {
#[cfg(not(web_sys_unstable_apis))]
tracing::warn!(
"eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work."
);
let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?;
runner.warm_up()?;
start_runner(runner)
}
/// Install event listeners to register different input events
/// and starts running the given [`AppRunner`].
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let mut runner_container = AppRunnerContainer {
runner: Arc::new(Mutex::new(app_runner)),
panicked: Arc::new(AtomicBool::new(false)),
events: Vec::with_capacity(20),
};
super::events::install_canvas_events(&mut runner_container)?;
super::events::install_document_events(&mut runner_container)?;
text_agent::install_text_agent(&mut runner_container)?;
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
// Disable all event handlers on panic
let previous_hook = std::panic::take_hook();
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
std::panic::set_hook(Box::new(move |panic_info| {
tracing::info!("egui disabled all event handlers due to panic");
runner_container.panicked.store(true, SeqCst);
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}));
Ok(runner_container.runner)
}
// ----------------------------------------------------------------------------
#[derive(Default)]
struct LocalStorage {}
impl epi::Storage for LocalStorage {
fn get_string(&self, key: &str) -> Option<String> {
local_storage_get(key)
}
fn set_string(&mut self, key: &str, value: String) {
local_storage_set(key, &value);
}
fn flush(&mut self) {}
}

View file

@ -1,538 +0,0 @@
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
struct IsDestroyed(pub bool);
pub fn paint_and_schedule(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
let mut runner_lock = runner_ref.lock();
let is_destroyed = runner_lock.is_destroyed.fetch();
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
runner_lock.needs_repaint.clear();
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
runner_lock
.needs_repaint
.repaint_after(repaint_after.as_secs_f64());
runner_lock.auto_save();
}
Ok(IsDestroyed(is_destroyed))
}
fn request_animation_frame(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(())
}
// Only paint and schedule if there has been no panic
if !panicked.load(Ordering::SeqCst) {
let is_destroyed = paint_if_needed(runner_ref)?;
if !is_destroyed.0 {
request_animation_frame(runner_ref.clone(), panicked)?;
}
}
Ok(())
}
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
runner_container.add_event_listener(
&document,
"keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| {
if event.is_composing() || event.key_code() == 229 {
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
return;
}
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
let key = event.key();
let egui_key = translate_key(&key);
if let Some(key) = egui_key {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
repeat: false, // egui will fill this in for us!
modifiers,
});
}
if !modifiers.ctrl
&& !modifiers.command
&& !should_ignore_key(&key)
// When text agent is shown, it sends text event instead.
&& text_agent::text_agent().hidden()
{
runner_lock.input.raw.events.push(egui::Event::Text(key));
}
runner_lock.needs_repaint.repaint_asap();
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
#[allow(clippy::if_same_then_else)]
let prevent_default = if egui_key == Some(Key::Tab) {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} else if egui_key == Some(Key::P) {
#[allow(clippy::needless_bool)]
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
} else {
false // let normal P:s through
}
} else if egui_wants_keyboard {
matches!(
event.key().as_str(),
"Backspace" // so we don't go back to previous page when deleting text
| "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58)
)
} else {
// We never want to prevent:
// * F5 / cmd-R (refresh)
// * cmd-shift-C (debug tools)
// * cmd/ctrl-c/v/x (or we stop copy/past/cut events)
false
};
// tracing::debug!(
// "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}",
// event.key().as_str(),
// egui_wants_keyboard,
// prevent_default
// );
if prevent_default {
event.prevent_default();
// event.stop_propagation();
}
},
)?;
runner_container.add_event_listener(
&document,
"keyup",
|event: web_sys::KeyboardEvent, mut runner_lock| {
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
if let Some(key) = translate_key(&event.key()) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: false,
repeat: false,
modifiers,
});
}
runner_lock.needs_repaint.repaint_asap();
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"paste",
|event: web_sys::ClipboardEvent, mut runner_lock| {
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
if !text.is_empty() {
runner_lock.input.raw.events.push(egui::Event::Paste(text));
runner_lock.needs_repaint.repaint_asap();
}
event.stop_propagation();
event.prevent_default();
}
}
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"cut",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Cut);
runner_lock.needs_repaint.repaint_asap();
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"copy",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Copy);
runner_lock.needs_repaint.repaint_asap();
},
)?;
for event_name in &["load", "pagehide", "pageshow", "resize"] {
runner_container.add_event_listener(
&window,
event_name,
|_: web_sys::Event, runner_lock| {
runner_lock.needs_repaint.repaint_asap();
},
)?;
}
runner_container.add_event_listener(
&window,
"hashchange",
|_: web_sys::Event, mut runner_lock| {
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
runner_lock.frame.info.web_info.location.hash = location_hash();
},
)?;
Ok(())
}
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
let prevent_default_events = [
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
"contextmenu",
// Allow users to use ctrl-p for e.g. a command palette
"afterprint",
];
for event_name in prevent_default_events {
let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
event.prevent_default();
// event.stop_propagation();
// tracing::debug!("Preventing event {:?}", event_name);
};
runner_container.add_event_listener(&canvas, event_name, closure)?;
}
runner_container.add_event_listener(
&canvas,
"mousedown",
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
}
event.stop_propagation();
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
},
)?;
runner_container.add_event_listener(
&canvas,
"mousemove",
|event: web_sys::MouseEvent, mut runner_lock| {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"mouseup",
|event: web_sys::MouseEvent, mut runner_lock| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner_lock.needs_repaint.repaint_asap();
text_agent::update_text_agent(runner_lock);
}
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"mouseleave",
|event: web_sys::MouseEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchstart",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchmove",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchend",
|event: web_sys::TouchEvent, mut runner_lock| {
if let Some(pos) = runner_lock.input.latest_touch_pos {
let modifiers = runner_lock.input.raw.modifiers;
// First release mouse to click:
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
});
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
// Finally, focus or blur text agent to toggle mobile keyboard:
text_agent::update_text_agent(runner_lock);
},
)?;
runner_container.add_event_listener(
&canvas,
"touchcancel",
|event: web_sys::TouchEvent, mut runner_lock| {
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"wheel",
|event: web_sys::WheelEvent, mut runner_lock| {
let scroll_multiplier = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PAGE => {
canvas_size_in_points(runner_lock.canvas_id()).y
}
web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL
};
let mut delta =
-scroll_multiplier * egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
// This if-statement is equivalent to how `Modifiers.command` is determined in
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
if event.ctrl_key() || event.meta_key() {
let factor = (delta.y / 200.0).exp();
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
} else {
if event.shift_key() {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = egui::vec2(delta.x + delta.y, 0.0);
}
runner_lock
.input
.raw
.events
.push(egui::Event::Scroll(delta));
}
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"dragover",
|event: web_sys::DragEvent, mut runner_lock| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
for i in 0..data_transfer.items().length() {
if let Some(item) = data_transfer.items().get(i) {
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
mime: item.type_(),
..Default::default()
});
}
}
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
}
},
)?;
runner_container.add_event_listener(
&canvas,
"dragleave",
|event: web_sys::DragEvent, mut runner_lock| {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(&canvas, "drop", {
let runner_ref = runner_container.runner.clone();
move |event: web_sys::DragEvent, mut runner_lock| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.repaint_asap();
// Unlock the runner so it can be locked after a future await point
drop(runner_lock);
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
let name = file.name();
let last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
tracing::debug!("Loading {:?} ({} bytes)…", name, file.size());
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
let runner_ref = runner_ref.clone();
let future = async move {
match future.await {
Ok(array_buffer) => {
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
tracing::debug!(
"Loaded {:?} ({} bytes).",
name,
bytes.len()
);
// Re-lock the mutex on the other side of the await point
let mut runner_lock = runner_ref.lock();
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
}
Err(err) => {
tracing::error!("Failed to read file: {:?}", err);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
event.stop_propagation();
event.prevent_default();
}
}
})?;
Ok(())
}

View file

@ -1,217 +0,0 @@
use super::{canvas_element, canvas_origin, AppRunner};
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
let rect = canvas.get_bounding_client_rect();
egui::Pos2 {
x: event.client_x() as f32 - rect.left() as f32,
y: event.client_y() as f32 - rect.top() as f32,
}
}
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
match event.button() {
0 => Some(egui::PointerButton::Primary),
1 => Some(egui::PointerButton::Middle),
2 => Some(egui::PointerButton::Secondary),
3 => Some(egui::PointerButton::Extra1),
4 => Some(egui::PointerButton::Extra2),
_ => None,
}
}
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
/// should not jump to a different position. Therefore, we do not calculate the average position
/// of all touches, but we keep using the same touch as long as it is available.
///
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> egui::Pos2 {
let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos {
// search for the touch we previously used for the position
// (unfortunately, `event.touches()` is not a rust collection):
(0..event.touches().length())
.into_iter()
.map(|i| event.touches().get(i).unwrap())
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos)
} else {
None
};
// Use the touch found above or pick the first, or return a default position if there is no
// touch at all. (The latter is not expected as the current method is only called when there is
// at least one touch.)
touch_for_pos
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
pos_from_touch(canvas_origin(canvas_id), &touch)
})
}
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 {
x: touch.page_x() as f32 - canvas_origin.x,
y: touch.page_y() as f32 - canvas_origin.y,
}
}
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
});
}
}
}
/// Web sends all keys as strings, so it is up to us to figure out if it is
/// a real text input or the name of a key.
pub fn should_ignore_key(key: &str) -> bool {
let is_function_key = key.starts_with('F') && key.len() > 1;
is_function_key
|| matches!(
key,
"Alt"
| "ArrowDown"
| "ArrowLeft"
| "ArrowRight"
| "ArrowUp"
| "Backspace"
| "CapsLock"
| "ContextMenu"
| "Control"
| "Delete"
| "End"
| "Enter"
| "Esc"
| "Escape"
| "GroupNext" // https://github.com/emilk/egui/issues/510
| "Help"
| "Home"
| "Insert"
| "Meta"
| "NumLock"
| "PageDown"
| "PageUp"
| "Pause"
| "ScrollLock"
| "Shift"
| "Tab"
)
}
/// Web sends all all keys as strings, so it is up to us to figure out if it is
/// a real text input or the name of a key.
pub fn translate_key(key: &str) -> Option<egui::Key> {
use egui::Key;
match key {
"ArrowDown" => Some(Key::ArrowDown),
"ArrowLeft" => Some(Key::ArrowLeft),
"ArrowRight" => Some(Key::ArrowRight),
"ArrowUp" => Some(Key::ArrowUp),
"Esc" | "Escape" => Some(Key::Escape),
"Tab" => Some(Key::Tab),
"Backspace" => Some(Key::Backspace),
"Enter" => Some(Key::Enter),
"Space" | " " => Some(Key::Space),
"Help" | "Insert" => Some(Key::Insert),
"Delete" => Some(Key::Delete),
"Home" => Some(Key::Home),
"End" => Some(Key::End),
"PageUp" => Some(Key::PageUp),
"PageDown" => Some(Key::PageDown),
"-" => Some(Key::Minus),
"+" | "=" => Some(Key::PlusEquals),
"0" => Some(Key::Num0),
"1" => Some(Key::Num1),
"2" => Some(Key::Num2),
"3" => Some(Key::Num3),
"4" => Some(Key::Num4),
"5" => Some(Key::Num5),
"6" => Some(Key::Num6),
"7" => Some(Key::Num7),
"8" => Some(Key::Num8),
"9" => Some(Key::Num9),
"a" | "A" => Some(Key::A),
"b" | "B" => Some(Key::B),
"c" | "C" => Some(Key::C),
"d" | "D" => Some(Key::D),
"e" | "E" => Some(Key::E),
"f" | "F" => Some(Key::F),
"g" | "G" => Some(Key::G),
"h" | "H" => Some(Key::H),
"i" | "I" => Some(Key::I),
"j" | "J" => Some(Key::J),
"k" | "K" => Some(Key::K),
"l" | "L" => Some(Key::L),
"m" | "M" => Some(Key::M),
"n" | "N" => Some(Key::N),
"o" | "O" => Some(Key::O),
"p" | "P" => Some(Key::P),
"q" | "Q" => Some(Key::Q),
"r" | "R" => Some(Key::R),
"s" | "S" => Some(Key::S),
"t" | "T" => Some(Key::T),
"u" | "U" => Some(Key::U),
"v" | "V" => Some(Key::V),
"w" | "W" => Some(Key::W),
"x" | "X" => Some(Key::X),
"y" | "Y" => Some(Key::Y),
"z" | "Z" => Some(Key::Z),
"F1" => Some(Key::F1),
"F2" => Some(Key::F2),
"F3" => Some(Key::F3),
"F4" => Some(Key::F4),
"F5" => Some(Key::F5),
"F6" => Some(Key::F6),
"F7" => Some(Key::F7),
"F8" => Some(Key::F8),
"F9" => Some(Key::F9),
"F10" => Some(Key::F10),
"F11" => Some(Key::F11),
"F12" => Some(Key::F12),
"F13" => Some(Key::F13),
"F14" => Some(Key::F14),
"F15" => Some(Key::F15),
"F16" => Some(Key::F16),
"F17" => Some(Key::F17),
"F18" => Some(Key::F18),
"F19" => Some(Key::F19),
"F20" => Some(Key::F20),
_ => None,
}
}
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
egui::Modifiers {
alt: event.alt_key(),
ctrl: event.ctrl_key(),
shift: event.shift_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
mac_cmd: event.meta_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
command: event.ctrl_key() || event.meta_key(),
}
}

View file

@ -1,258 +0,0 @@
//! [`egui`] bindings for web apps (compiling to WASM).
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
pub mod backend;
mod events;
mod input;
pub mod screen_reader;
pub mod storage;
mod text_agent;
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
mod web_painter;
#[cfg(feature = "glow")]
mod web_painter_glow;
#[cfg(feature = "glow")]
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
#[cfg(feature = "wgpu")]
mod web_painter_wgpu;
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*;
pub use events::*;
pub use storage::*;
use std::collections::BTreeMap;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use egui::Vec2;
use wasm_bindgen::prelude::*;
use web_sys::EventTarget;
use input::*;
use crate::Theme;
// ----------------------------------------------------------------------------
/// Current time in seconds (since undefined point in time).
///
/// Monotonically increasing.
pub fn now_sec() -> f64 {
web_sys::window()
.expect("should have a Window")
.performance()
.expect("should have a Performance")
.now()
/ 1000.0
}
#[allow(dead_code)]
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
let window = web_sys::window()?;
Some(egui::vec2(
window.inner_width().ok()?.as_f64()? as f32,
window.inner_height().ok()?.as_f64()? as f32,
))
}
pub fn native_pixels_per_point() -> f32 {
let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32;
if pixels_per_point > 0.0 && pixels_per_point.is_finite() {
pixels_per_point
} else {
1.0
}
}
pub fn system_theme() -> Option<Theme> {
let dark_mode = web_sys::window()?
.match_media("(prefers-color-scheme: dark)")
.ok()??
.matches();
Some(if dark_mode { Theme::Dark } else { Theme::Light })
}
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
}
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
canvas_element(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
let rect = canvas_element(canvas_id)
.unwrap()
.get_bounding_client_rect();
egui::pos2(rect.left() as f32, rect.top() as f32)
}
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
let canvas = canvas_element(canvas_id).unwrap();
let pixels_per_point = native_pixels_per_point();
egui::vec2(
canvas.width() as f32 / pixels_per_point,
canvas.height() as f32 / pixels_per_point,
)
}
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
let canvas = canvas_element(canvas_id)?;
let parent = canvas.parent_element()?;
let width = parent.scroll_width();
let height = parent.scroll_height();
let canvas_real_size = Vec2 {
x: width as f32,
y: height as f32,
};
if width <= 0 || height <= 0 {
tracing::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height);
}
let pixels_per_point = native_pixels_per_point();
let max_size_pixels = pixels_per_point * max_size_points;
let canvas_size_pixels = pixels_per_point * canvas_real_size;
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
let canvas_size_points = canvas_size_pixels / pixels_per_point;
// Make sure that the height and width are always even numbers.
// otherwise, the page renders blurry on some platforms.
// See https://github.com/emilk/egui/issues/103
fn round_to_even(v: f32) -> f32 {
(v / 2.0).round() * 2.0
}
canvas
.style()
.set_property(
"width",
&format!("{}px", round_to_even(canvas_size_points.x)),
)
.ok()?;
canvas
.style()
.set_property(
"height",
&format!("{}px", round_to_even(canvas_size_points.y)),
)
.ok()?;
canvas.set_width(round_to_even(canvas_size_pixels.x) as u32);
canvas.set_height(round_to_even(canvas_size_pixels.y) as u32);
Some(())
}
// ----------------------------------------------------------------------------
pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
let document = web_sys::window()?.document()?;
document
.body()?
.style()
.set_property("cursor", cursor_web_name(cursor))
.ok()
}
#[cfg(web_sys_unstable_apis)]
pub fn set_clipboard_text(s: &str) {
if let Some(window) = web_sys::window() {
if let Some(clipboard) = window.navigator().clipboard() {
let promise = clipboard.write_text(s);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
tracing::error!("Copy/cut action denied: {:?}", err);
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str {
match cursor {
egui::CursorIcon::Alias => "alias",
egui::CursorIcon::AllScroll => "all-scroll",
egui::CursorIcon::Cell => "cell",
egui::CursorIcon::ContextMenu => "context-menu",
egui::CursorIcon::Copy => "copy",
egui::CursorIcon::Crosshair => "crosshair",
egui::CursorIcon::Default => "default",
egui::CursorIcon::Grab => "grab",
egui::CursorIcon::Grabbing => "grabbing",
egui::CursorIcon::Help => "help",
egui::CursorIcon::Move => "move",
egui::CursorIcon::NoDrop => "no-drop",
egui::CursorIcon::None => "none",
egui::CursorIcon::NotAllowed => "not-allowed",
egui::CursorIcon::PointingHand => "pointer",
egui::CursorIcon::Progress => "progress",
egui::CursorIcon::ResizeHorizontal => "ew-resize",
egui::CursorIcon::ResizeNeSw => "nesw-resize",
egui::CursorIcon::ResizeNwSe => "nwse-resize",
egui::CursorIcon::ResizeVertical => "ns-resize",
egui::CursorIcon::ResizeEast => "e-resize",
egui::CursorIcon::ResizeSouthEast => "se-resize",
egui::CursorIcon::ResizeSouth => "s-resize",
egui::CursorIcon::ResizeSouthWest => "sw-resize",
egui::CursorIcon::ResizeWest => "w-resize",
egui::CursorIcon::ResizeNorthWest => "nw-resize",
egui::CursorIcon::ResizeNorth => "n-resize",
egui::CursorIcon::ResizeNorthEast => "ne-resize",
egui::CursorIcon::ResizeColumn => "col-resize",
egui::CursorIcon::ResizeRow => "row-resize",
egui::CursorIcon::Text => "text",
egui::CursorIcon::VerticalText => "vertical-text",
egui::CursorIcon::Wait => "wait",
egui::CursorIcon::ZoomIn => "zoom-in",
egui::CursorIcon::ZoomOut => "zoom-out",
}
}
pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
let name = if new_tab { "_blank" } else { "_self" };
web_sys::window()?
.open_with_url_and_target(url, name)
.ok()?;
Some(())
}
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
///
/// Percent decoded
pub fn location_hash() -> String {
percent_decode(
&web_sys::window()
.unwrap()
.location()
.hash()
.unwrap_or_default(),
)
}
pub fn percent_decode(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8_lossy()
.to_string()
}

View file

@ -1,49 +0,0 @@
pub struct ScreenReader {
#[cfg(feature = "tts")]
tts: Option<tts::Tts>,
}
#[cfg(not(feature = "tts"))]
#[allow(clippy::derivable_impls)] // False positive
impl Default for ScreenReader {
fn default() -> Self {
Self {}
}
}
#[cfg(feature = "tts")]
impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::Tts::default() {
Ok(screen_reader) => {
tracing::debug!("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
tracing::warn!("Failed to load screen reader: {}", err);
None
}
};
Self { tts }
}
}
impl ScreenReader {
#[cfg(not(feature = "tts"))]
#[allow(clippy::unused_self)]
pub fn speak(&mut self, _text: &str) {}
#[cfg(feature = "tts")]
pub fn speak(&mut self, text: &str) {
if text.is_empty() {
return;
}
if let Some(tts) = &mut self.tts {
tracing::debug!("Speaking: {:?}", text);
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
tracing::warn!("Failed to read: {}", err);
}
}
}
}

View file

@ -1,43 +0,0 @@
fn local_storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok()?
}
pub fn local_storage_get(key: &str) -> Option<String> {
local_storage().map(|storage| storage.get_item(key).ok())??
}
pub fn local_storage_set(key: &str, value: &str) {
local_storage().map(|storage| storage.set_item(key, value));
}
#[cfg(feature = "persistence")]
pub fn load_memory(ctx: &egui::Context) {
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
match ron::from_str(&memory_string) {
Ok(memory) => {
ctx.memory_mut(|m| *m = memory);
}
Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err);
}
}
}
}
#[cfg(not(feature = "persistence"))]
pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) {
match ctx.memory(|mem| ron::to_string(mem)) {
Ok(ron) => {
local_storage_set("egui_memory_ron", &ron);
}
Err(err) => {
tracing::error!("Failed to serialize memory as RON: {}", err);
}
}
}
#[cfg(not(feature = "persistence"))]
pub fn save_memory(_: &egui::Context) {}

View file

@ -1,225 +0,0 @@
//! The text agent is an `<input>` element used to trigger
//! mobile keyboard and IME input.
use super::{canvas_element, AppRunner, AppRunnerContainer};
use egui::mutex::MutexGuard;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id(AGENT_ID)
.unwrap()
.dyn_into()
.unwrap()
}
/// Text event handler,
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
let input = std::rc::Rc::new(input);
input.set_id(AGENT_ID);
let is_composing = Rc::new(Cell::new(false));
{
let style = input.style();
// Transparent
style.set_property("opacity", "0").unwrap();
// Hide under canvas
style.set_property("z-index", "-1").unwrap();
}
// Set size as small as possible, in case user may click on it.
input.set_size(1);
input.set_autofocus(true);
input.set_hidden(true);
// When IME is off
runner_container.add_event_listener(&input, "input", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::InputEvent, mut runner_lock| {
let text = input_clone.value();
if !text.is_empty() && !is_composing.get() {
input_clone.set_value("");
runner_lock.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.repaint_asap();
}
}
})?;
{
// When IME is on, handle composition event
runner_container.add_event_listener(&input, "compositionstart", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
is_composing.set(true);
input_clone.set_value("");
runner_lock
.input
.raw
.events
.push(egui::Event::CompositionStart);
runner_lock.needs_repaint.repaint_asap();
}
})?;
runner_container.add_event_listener(
&input,
"compositionupdate",
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap();
}
},
)?;
runner_container.add_event_listener(&input, "compositionend", {
let input_clone = input.clone();
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
is_composing.set(false);
input_clone.set_value("");
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.repaint_asap();
}
}
})?;
}
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
runner_container.add_event_listener(
&input,
"focusout",
move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
},
)?;
body.append_child(&input)?;
Ok(())
}
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
let canvas_style = canvas_element(runner.canvas_id())?.style();
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
if is_already_editing {
input.set_hidden(false);
input.focus().ok()?;
// Move up canvas so that text edit is shown at ~30% of screen height.
// Only on touch screens, when keyboard popups.
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
let window_height = window.inner_height().ok()?.as_f64()? as f32;
let current_rel = latest_touch_pos.y / window_height;
// estimated amount of screen covered by keyboard
let keyboard_fraction = 0.5;
if current_rel > keyboard_fraction {
// below the keyboard
let target_rel = 0.3;
// Note: `delta` is negative, since we are moving the canvas UP
let delta = target_rel - current_rel;
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
let new_pos_percent = format!("{}%", (delta * 100.0).round());
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", &new_pos_percent).ok()?;
}
}
}
} else {
// Drop runner lock
drop(runner);
// Holding the runner lock while calling input.blur() causes a panic.
// This is most probably caused by the browser running the event handler
// for the triggered blur event synchronously, meaning that the mutex
// lock does not get dropped by the time another event handler is called.
//
// Why this didn't exist before #1290 is a mystery to me, but it exists now
// and this apparently is the fix for it
//
// ¯\_(ツ)_/¯ - @DusterTheFirst
input.blur().ok()?;
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
}
Some(())
}
/// If context is running under mobile device?
fn is_mobile() -> Option<bool> {
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
Some(is_mobile)
}
// Move text agent to text cursor's position, on desktop/laptop,
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
let canvas = canvas_element(canvas_id)?;
let bounding_rect = text_agent().get_bounding_client_rect();
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
// Canvas is translated 50% horizontally in html.
let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?;
style.set_property("top", &format!("{}px", y)).ok()?;
style.set_property("left", &format!("{}px", x)).ok()
})
} else {
style.set_property("position", "absolute").ok()?;
style.set_property("top", "0px").ok()?;
style.set_property("left", "0px").ok()
}
}

View file

@ -1,29 +0,0 @@
use wasm_bindgen::JsValue;
/// Renderer for a browser canvas.
/// As of writing we're not allowing to decide on the painter at runtime,
/// therefore this trait is merely there for specifying and documenting the interface.
pub(crate) trait WebPainter {
// Create a new web painter targeting a given canvas.
// fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String>
// where
// Self: Sized;
/// Id of the canvas in use.
fn canvas_id(&self) -> &str;
/// Maximum size of a texture in one direction.
fn max_texture_side(&self) -> usize;
/// Update all internal textures and paint gui.
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue>;
/// Destroy all resources.
fn destroy(&mut self);
}

View file

@ -1,184 +0,0 @@
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui_glow::glow;
use crate::{WebGlContextOption, WebOptions};
use super::web_painter::WebPainter;
pub(crate) struct WebPainterGlow {
canvas: HtmlCanvasElement,
canvas_id: String,
painter: egui_glow::Painter,
}
impl WebPainterGlow {
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
self.painter.gl()
}
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
let canvas = super::canvas_element_or_die(canvas_id);
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
.map_err(|error| format!("Error starting glow painter: {}", error))?;
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
painter,
})
}
}
impl WebPainter for WebPainterGlow {
fn max_texture_side(&self) -> usize {
self.painter.max_texture_side()
}
fn canvas_id(&self) -> &str {
&self.canvas_id
}
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
for (id, image_delta) in &textures_delta.set {
self.painter.set_texture(*id, image_delta);
}
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
for &id in &textures_delta.free {
self.painter.free_texture(id);
}
Ok(())
}
fn destroy(&mut self) {
self.painter.destroy();
}
}
/// Returns glow context and shader prefix.
fn init_glow_context_from_canvas(
canvas: &HtmlCanvasElement,
options: WebGlContextOption,
) -> Result<(glow::Context, &'static str), String> {
let result = match options {
// Force use WebGl1
WebGlContextOption::WebGl1 => init_webgl1(canvas),
// Force use WebGl2
WebGlContextOption::WebGl2 => init_webgl2(canvas),
// Trying WebGl2 first
WebGlContextOption::BestFirst => init_webgl2(canvas).or_else(|| init_webgl1(canvas)),
// Trying WebGl1 first (useful for testing).
WebGlContextOption::CompatibilityFirst => {
init_webgl1(canvas).or_else(|| init_webgl2(canvas))
}
};
if let Some(result) = result {
Ok(result)
} else {
Err("WebGL isn't supported".into())
}
}
fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
let gl1_ctx = canvas
.get_context("webgl")
.expect("Failed to query about WebGL2 context");
let gl1_ctx = gl1_ctx?;
tracing::debug!("WebGL1 selected.");
let gl1_ctx = gl1_ctx
.dyn_into::<web_sys::WebGlRenderingContext>()
.unwrap();
let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) {
tracing::debug!("Enabling webkitGTK brightening workaround.");
"#define APPLY_BRIGHTENING_GAMMA"
} else {
""
};
let gl = glow::Context::from_webgl1_context(gl1_ctx);
Some((gl, shader_prefix))
}
fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
let gl2_ctx = canvas
.get_context("webgl2")
.expect("Failed to query about WebGL2 context");
let gl2_ctx = gl2_ctx?;
tracing::debug!("WebGL2 selected.");
let gl2_ctx = gl2_ctx
.dyn_into::<web_sys::WebGl2RenderingContext>()
.unwrap();
let gl = glow::Context::from_webgl2_context(gl2_ctx);
let shader_prefix = "";
Some((gl, shader_prefix))
}
fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794
// detect WebKitGTK
// WebKitGTK use WebKit default unmasked vendor and renderer
// but safari use same vendor and renderer
// so exclude "Mac OS X" user-agent.
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
}
/// detecting Safari and `webkitGTK`.
///
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
///
/// If we detect safari or `webkitGTKs` returns true.
///
/// This function used to avoid displaying linear color with `sRGB` supported systems.
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
// TODO(emilk): do something smart based on user agent?
if gl
.get_extension("WEBGL_debug_renderer_info")
.unwrap()
.is_some()
{
if let Ok(renderer) =
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
{
if let Some(renderer) = renderer.as_string() {
if renderer.contains("Apple") {
return true;
}
}
}
}
false
}

View file

@ -1,282 +0,0 @@
use std::sync::Arc;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui::mutex::RwLock;
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
use crate::WebOptions;
use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
canvas_id: String,
surface: wgpu::Surface,
surface_configuration: wgpu::SurfaceConfiguration,
limits: wgpu::Limits,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
pub fn generate_depth_texture_view(
&self,
render_state: &RenderState,
width_in_pixels: u32,
height_in_pixels: u32,
) -> Option<wgpu::TextureView> {
let device = &render_state.device;
self.depth_format.map(|depth_format| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
width: width_in_pixels,
height: height_in_pixels,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
tracing::debug!("Creating wgpu painter");
let canvas = super::canvas_element_or_die(canvas_id);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: options.wgpu_options.backends,
dx12_shader_compiler: Default::default(),
});
let surface = instance
.create_surface_from_canvas(&canvas)
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: options.wgpu_options.power_preference,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?;
let (device, queue) = adapter
.request_device(
&options.wgpu_options.device_descriptor,
None, // Capture doesn't work in the browser environment.
)
.await
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
let target_format =
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
let depth_format = options.wgpu_options.depth_format;
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
let render_state = RenderState {
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
renderer: Arc::new(RwLock::new(renderer)),
};
let surface_configuration = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: target_format,
width: 0,
height: 0,
present_mode: options.wgpu_options.present_mode,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![target_format],
};
tracing::debug!("wgpu painter initialized.");
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
render_state: Some(render_state),
surface,
surface_configuration,
depth_format,
depth_texture_view: None,
limits: options.wgpu_options.device_descriptor.limits.clone(),
on_surface_error: options.wgpu_options.on_surface_error.clone(),
})
}
}
impl WebPainter for WebPainterWgpu {
fn canvas_id(&self) -> &str {
&self.canvas_id
}
fn max_texture_side(&self) -> usize {
self.limits.max_texture_dimension_2d as _
}
fn paint_and_update_textures(
&mut self,
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> {
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
let render_state = if let Some(render_state) = &self.render_state {
render_state
} else {
return Err(JsValue::from_str(
"Can't paint, wgpu renderer was already disposed",
));
};
let mut encoder =
render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("egui_webpainter_paint_and_update_textures"),
});
// Upload all resources for the GPU.
let screen_descriptor = ScreenDescriptor {
size_in_pixels,
pixels_per_point,
};
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
renderer.update_buffers(
&render_state.device,
&render_state.queue,
&mut encoder,
clipped_primitives,
&screen_descriptor,
)
};
// Resize surface if needed
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
let frame = if is_zero_sized_surface {
None
} else {
if size_in_pixels[0] != self.surface_configuration.width
|| size_in_pixels[1] != self.surface_configuration.height
{
self.surface_configuration.width = size_in_pixels[0];
self.surface_configuration.height = size_in_pixels[1];
self.surface
.configure(&render_state.device, &self.surface_configuration);
self.depth_texture_view = self.generate_depth_texture_view(
render_state,
size_in_pixels[0],
size_in_pixels[1],
);
}
let frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
#[allow(clippy::single_match_else)]
Err(e) => match (*self.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
return Ok(());
}
SurfaceErrorAction::SkipFrame => {
return Ok(());
}
},
};
{
let renderer = render_state.renderer.read();
let frame_view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: true,
},
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: false,
}),
stencil_ops: None,
}
}),
label: Some("egui_render"),
});
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
}
Some(frame)
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state.queue.submit(
user_cmd_bufs
.into_iter()
.chain(std::iter::once(encoder.finish())),
);
if let Some(frame) = frame {
frame.present();
}
Ok(())
}
fn destroy(&mut self) {
self.render_state = None;
}
}

View file

@ -1,37 +0,0 @@
# Changelog for egui-wgpu
All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased
## 0.21.0 - 2023-02-08
* Update to `wgpu` 0.15 ([#2629](https://github.com/emilk/egui/pull/2629))
* Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)).
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
* `winit::Painter` now supports transparent backbuffer ([#2684](https://github.com/emilk/egui/pull/2684)).
## 0.20.0 - 2022-12-08 - web support
* Renamed `RenderPass` to `Renderer`.
* Renamed `RenderPass::execute` to `RenderPass::render`.
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
* Reexported `Renderer`.
* You can now use `egui-wgpu` on web, using WebGL ([#2107](https://github.com/emilk/egui/pull/2107)).
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
* Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)).
* `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198))
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
## 0.19.0 - 2022-08-20
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
* Make `RenderPass` `Send` and `Sync` ([#1883](https://github.com/emilk/egui/pull/1883)).
## 0.18.0 - 2022-05-15
First published version since moving the code into the `egui` repository from <https://github.com/LU15W1R7H/eww>.

View file

@ -1,56 +0,0 @@
[package]
name = "egui-wgpu"
version = "0.21.0"
description = "Bindings for using egui natively using the wgpu library"
authors = [
"Nils Hasenbanck <nils@hasenbanck.de>",
"embotech <opensource@embotech.com>",
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
]
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"**/*.wgsl",
"Cargo.toml",
]
[package.metadata.docs.rs]
all-features = true
[features]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Enable [`winit`](https://docs.rs/winit) integration.
winit = ["dep:winit"]
[dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
"bytemuck",
] }
bytemuck = "1.7"
tracing = { version = "0.1", default-features = false, features = ["std"] }
type-map = "0.5.0"
wgpu = "0.15.0"
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
winit = { version = "0.28", optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = { version = "0.14", optional = true }

View file

@ -1,10 +0,0 @@
# egui-wgpu
[![Latest version](https://img.shields.io/crates/v/egui-wgpu.svg)](https://crates.io/crates/egui-wgpu)
[![Documentation](https://docs.rs/egui-wgpu/badge.svg)](https://docs.rs/egui-wgpu)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
This was originally hosted at https://github.com/hasenbanck/egui_wgpu_backend

View file

@ -1,91 +0,0 @@
// Vertex shader bindings
struct VertexOutput {
@location(0) tex_coord: vec2<f32>,
@location(1) color: vec4<f32>, // gamma 0-1
@builtin(position) position: vec4<f32>,
};
struct Locals {
screen_size: vec2<f32>,
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: vec2<u32>,
};
@group(0) @binding(0) var<uniform> r_locals: Locals;
// 0-1 linear from 0-1 sRGB gamma
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
let lower = srgb / vec3<f32>(12.92);
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
return select(higher, lower, cutoff);
}
// 0-1 sRGB gamma from 0-1 linear
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
let cutoff = rgb < vec3<f32>(0.0031308);
let lower = rgb * vec3<f32>(12.92);
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
return select(higher, lower, cutoff);
}
// 0-1 sRGBA gamma from 0-1 linear
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
}
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
fn unpack_color(color: u32) -> vec4<f32> {
return vec4<f32>(
f32(color & 255u),
f32((color >> 8u) & 255u),
f32((color >> 16u) & 255u),
f32((color >> 24u) & 255u),
) / 255.0;
}
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
return vec4<f32>(
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
0.0,
1.0,
);
}
@vertex
fn vs_main(
@location(0) a_pos: vec2<f32>,
@location(1) a_tex_coord: vec2<f32>,
@location(2) a_color: u32,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coord = a_tex_coord;
out.color = unpack_color(a_color);
out.position = position_from_screen(a_pos);
return out;
}
// Fragment shader bindings
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return vec4<f32>(linear_from_gamma_rgb(out_color_gamma.rgb), out_color_gamma.a);
}
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return out_color_gamma;
}

View file

@ -1,155 +0,0 @@
//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(unsafe_code)]
pub use wgpu;
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
pub mod renderer;
pub use renderer::CallbackFn;
pub use renderer::Renderer;
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
use std::sync::Arc;
use epaint::mutex::RwLock;
/// Access to the render state for egui.
#[derive(Clone)]
pub struct RenderState {
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: wgpu::TextureFormat,
pub renderer: Arc<RwLock<Renderer>>,
}
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
/// Instructs egui to recreate the surface, then skip the current frame.
RecreateSurface,
}
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
#[derive(Clone)]
pub struct WgpuConfiguration {
/// Configuration passed on device request.
pub device_descriptor: wgpu::DeviceDescriptor<'static>,
/// Backends that should be supported (wgpu will pick one of these)
pub backends: wgpu::Backends,
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Power preference for the adapter.
pub power_preference: wgpu::PowerPreference,
/// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
pub depth_format: Option<wgpu::TextureFormat>,
}
impl Default for WgpuConfiguration {
fn default() -> Self {
Self {
device_descriptor: wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
features: wgpu::Features::default(),
limits: wgpu::Limits::default(),
},
backends: wgpu::Backends::PRIMARY | wgpu::Backends::GL,
present_mode: wgpu::PresentMode::AutoVsync,
power_preference: wgpu::PowerPreference::HighPerformance,
depth_format: None,
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
} else {
tracing::warn!("Dropped frame with error: {err}");
}
SurfaceErrorAction::SkipFrame
}),
}
}
}
/// Find the framebuffer format that egui prefers
pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
for &format in formats {
if matches!(
format,
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
) {
return format;
}
}
formats[0] // take the first
}
// maybe use this-error?
#[derive(Debug)]
pub enum WgpuError {
DeviceError(wgpu::RequestDeviceError),
SurfaceError(wgpu::CreateSurfaceError),
}
impl std::fmt::Display for WgpuError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
impl std::error::Error for WgpuError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WgpuError::DeviceError(e) => e.source(),
WgpuError::SurfaceError(e) => e.source(),
}
}
}
impl From<wgpu::RequestDeviceError> for WgpuError {
fn from(e: wgpu::RequestDeviceError) -> Self {
Self::DeviceError(e)
}
}
impl From<wgpu::CreateSurfaceError> for WgpuError {
fn from(e: wgpu::CreateSurfaceError) -> Self {
Self::SurfaceError(e)
}
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;

View file

@ -1,931 +0,0 @@
#![allow(unsafe_code)]
use std::num::NonZeroU64;
use std::ops::Range;
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
use type_map::concurrent::TypeMap;
use wgpu;
use wgpu::util::DeviceExt as _;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU
/// rendering.
///
/// The callback is composed of two functions: `prepare` and `paint`:
/// - `prepare` is called every frame before `paint`, and can use the passed-in
/// [`wgpu::Device`] and [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers.
/// - `paint` is called after `prepare` and is given access to the [`wgpu::RenderPass`] so
/// that it can issue draw commands into the same [`wgpu::RenderPass`] that is used for
/// all other egui elements.
///
/// The final argument of both the `prepare` and `paint` callbacks is a the
/// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources].
/// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to
/// store buffers, pipelines, and other information that needs to be accessed during the render
/// pass.
///
/// # Example
///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub struct CallbackFn {
prepare: Box<PrepareCallback>,
paint: Box<PaintCallback>,
}
type PrepareCallback = dyn Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send;
type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
impl Default for CallbackFn {
fn default() -> Self {
CallbackFn {
prepare: Box::new(|_, _, _, _| Vec::new()),
paint: Box::new(|_, _, _| ()),
}
}
}
impl CallbackFn {
pub fn new() -> Self {
Self::default()
}
/// Set the prepare callback.
///
/// The passed-in `CommandEncoder` is egui's and can be used directly to register
/// wgpu commands for simple use cases.
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
/// rendering itself.
///
/// For more complicated use cases, one can also return a list of arbitrary
/// `CommandBuffer`s and have complete control over how they get created and fed.
/// In particular, this gives an opportunity to parallelize command registration and
/// prevents a faulty callback from poisoning the main wgpu pipeline.
///
/// When using eframe, the main egui command buffer, as well as all user-defined
/// command buffers returned by this function, are guaranteed to all be submitted
/// at once in a single call.
pub fn prepare<F>(mut self, prepare: F) -> Self
where
F: Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send
+ 'static,
{
self.prepare = Box::new(prepare) as _;
self
}
/// Set the paint callback
pub fn paint<F>(mut self, paint: F) -> Self
where
F: for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap)
+ Sync
+ Send
+ 'static,
{
self.paint = Box::new(paint) as _;
self
}
}
/// Information about the screen used for rendering.
pub struct ScreenDescriptor {
/// Size of the window in physical pixels.
pub size_in_pixels: [u32; 2],
/// HiDPI scale factor (pixels per point).
pub pixels_per_point: f32,
}
impl ScreenDescriptor {
/// size in "logical" points
fn screen_size_in_points(&self) -> [f32; 2] {
[
self.size_in_pixels[0] as f32 / self.pixels_per_point,
self.size_in_pixels[1] as f32 / self.pixels_per_point,
]
}
}
/// Uniform buffer used when rendering.
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct UniformBuffer {
screen_size_in_points: [f32; 2],
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: [u32; 2],
}
struct SlicedBuffer {
buffer: wgpu::Buffer,
slices: Vec<Range<wgpu::BufferAddress>>,
capacity: wgpu::BufferAddress,
}
/// Renderer for a egui based GUI.
pub struct Renderer {
pipeline: wgpu::RenderPipeline,
index_buffer: SlicedBuffer,
vertex_buffer: SlicedBuffer,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
/// sampler.
textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
next_user_texture_id: u64,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
/// pipelines that must have the lifetime of the renderpass.
pub paint_callback_resources: TypeMap,
}
impl Renderer {
/// Creates a renderer for a egui UI.
///
/// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
pub fn new(
device: &wgpu::Device,
output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
) -> Self {
crate::profile_function!();
let shader = wgpu::ShaderModuleDescriptor {
label: Some("egui"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
};
let module = device.create_shader_module(shader);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("egui_uniform_buffer"),
contents: bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points: [0.0, 0.0],
_padding: Default::default(),
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("egui_uniform_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
}],
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("egui_uniform_bind_group"),
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &uniform_buffer,
offset: 0,
size: None,
}),
}],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("egui_texture_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
push_constant_ranges: &[],
});
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("egui_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
entry_point: "vs_main",
module: &module,
buffers: &[wgpu::VertexBufferLayout {
array_stride: 5 * 4,
step_mode: wgpu::VertexStepMode::Vertex,
// 0: vec2 position
// 1: vec2 texture coordinates
// 2: uint color
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
}],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
unclipped_depth: false,
conservative: false,
cull_mode: None,
front_face: wgpu::FrontFace::default(),
polygon_mode: wgpu::PolygonMode::default(),
strip_index_format: None,
},
depth_stencil,
multisample: wgpu::MultisampleState {
alpha_to_coverage_enabled: false,
count: msaa_samples,
mask: !0,
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: if output_color_format.describe().srgb {
tracing::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
"fs_main_linear_framebuffer"
} else {
"fs_main_gamma_framebuffer" // this is what we prefer
},
targets: &[Some(wgpu::ColorTargetState {
format: output_color_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
});
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<Vertex>() * 1024) as _;
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<u32>() * 1024 * 3) as _;
Self {
pipeline,
vertex_buffer: SlicedBuffer {
buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
slices: Vec::with_capacity(64),
capacity: VERTEX_BUFFER_START_CAPACITY,
},
index_buffer: SlicedBuffer {
buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
slices: Vec::with_capacity(64),
capacity: INDEX_BUFFER_START_CAPACITY,
},
uniform_buffer,
uniform_bind_group,
texture_bind_group_layout,
textures: HashMap::new(),
next_user_texture_id: 0,
samplers: HashMap::new(),
paint_callback_resources: TypeMap::default(),
}
}
/// Executes the egui renderer onto an existing wgpu renderpass.
pub fn render<'rp>(
&'rp self,
render_pass: &mut wgpu::RenderPass<'rp>,
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) {
crate::profile_function!();
let pixels_per_point = screen_descriptor.pixels_per_point;
let size_in_pixels = screen_descriptor.size_in_pixels;
// Whether or not we need to reset the render pass because a paint callback has just
// run.
let mut needs_reset = true;
let mut index_buffer_slices = self.index_buffer.slices.iter();
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
for epaint::ClippedPrimitive {
clip_rect,
primitive,
} in paint_jobs
{
if needs_reset {
render_pass.set_viewport(
0.0,
0.0,
size_in_pixels[0] as f32,
size_in_pixels[1] as f32,
0.0,
1.0,
);
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
needs_reset = false;
}
{
let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
if rect.width == 0 || rect.height == 0 {
// Skip rendering zero-sized clip areas.
if let Primitive::Mesh(_) = primitive {
// If this is a mesh, we need to advance the index and vertex buffer iterators:
index_buffer_slices.next().unwrap();
vertex_buffer_slices.next().unwrap();
}
continue;
}
render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
}
match primitive {
Primitive::Mesh(mesh) => {
let index_buffer_slice = index_buffer_slices.next().unwrap();
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) {
render_pass.set_bind_group(1, bind_group, &[]);
render_pass.set_index_buffer(
self.index_buffer.buffer.slice(index_buffer_slice.clone()),
wgpu::IndexFormat::Uint32,
);
render_pass.set_vertex_buffer(
0,
self.vertex_buffer.buffer.slice(vertex_buffer_slice.clone()),
);
render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
} else {
tracing::warn!("Missing texture: {:?}", mesh.texture_id);
}
}
Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c
} else {
// We already warned in the `prepare` callback
continue;
};
if callback.rect.is_positive() {
crate::profile_scope!("callback");
needs_reset = true;
{
// We're setting a default viewport for the render pass as a
// courtesy for the user, so that they don't have to think about
// it in the simple case where they just want to fill the whole
// paint area.
//
// The user still has the possibility of setting their own custom
// viewport during the paint callback, effectively overriding this
// one.
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
render_pass.set_viewport(
min.x,
min.y,
max.x - min.x,
max.y - min.y,
0.0,
1.0,
);
}
(cbfn.paint)(
PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
},
render_pass,
&self.paint_callback_resources,
);
}
}
}
}
render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
}
/// Should be called before `render()`.
pub fn update_texture(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: epaint::TextureId,
image_delta: &epaint::ImageDelta,
) {
crate::profile_function!();
let width = image_delta.image.width() as u32;
let height = image_delta.image.height() as u32;
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
let data_color32 = match &image_delta.image {
epaint::ImageData::Color(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
Cow::Owned(image.srgba_pixels(None).collect::<Vec<_>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
let queue_write_data_to_texture = |texture, origin| {
queue.write_texture(
wgpu::ImageCopyTexture {
texture,
mip_level: 0,
origin,
aspect: wgpu::TextureAspect::All,
},
data_bytes,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: NonZeroU32::new(4 * width),
rows_per_image: NonZeroU32::new(height),
},
size,
);
};
if let Some(pos) = image_delta.pos {
// update the existing texture
let (texture, _bind_group) = self
.textures
.get(&id)
.expect("Tried to update a texture that has not been allocated yet.");
let origin = wgpu::Origin3d {
x: pos[0] as u32,
y: pos[1] as u32,
z: 0,
};
queue_write_data_to_texture(
texture.as_ref().expect("Tried to update user texture."),
origin,
);
} else {
// allocate a new texture
// Use same label for all resources associated with this texture id (no point in retyping the type)
let label_str = format!("egui_texid_{:?}", id);
let label = Some(label_str.as_str());
let texture = device.create_texture(&wgpu::TextureDescriptor {
label,
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
});
let sampler = self
.samplers
.entry(image_delta.options)
.or_insert_with(|| create_sampler(image_delta.options, device));
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&texture.create_view(&wgpu::TextureViewDescriptor::default()),
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
let origin = wgpu::Origin3d::ZERO;
queue_write_data_to_texture(&texture, origin);
self.textures.insert(id, (Some(texture), bind_group));
};
}
pub fn free_texture(&mut self, id: &epaint::TextureId) {
self.textures.remove(id);
}
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
///
/// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
pub fn texture(
&self,
id: &epaint::TextureId,
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
self.textures.get(id)
}
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
///
/// This enables the application to reference the texture inside an image ui element.
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
/// the texture format `TextureFormat::Rgba8UnormSrgb` and
/// Texture usage `TextureUsage::SAMPLED`.
pub fn register_native_texture(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
) -> epaint::TextureId {
self.register_native_texture_with_sampler_options(
device,
texture,
wgpu::SamplerDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
mag_filter: texture_filter,
min_filter: texture_filter,
..Default::default()
},
)
}
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
///
/// This enables applications to reuse `TextureId`s.
pub fn update_egui_texture_from_wgpu_texture(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
id: epaint::TextureId,
) {
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
device,
texture,
wgpu::SamplerDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
mag_filter: texture_filter,
min_filter: texture_filter,
..Default::default()
},
id,
);
}
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to specify individual minification/magnification filters as well as
/// custom mipmap and tiling options.
///
/// The `Texture` must have the format `TextureFormat::Rgba8UnormSrgb` and usage
/// `TextureUsage::SAMPLED`. Any compare function supplied in the `SamplerDescriptor` will be
/// ignored.
#[allow(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
) -> epaint::TextureId {
crate::profile_function!();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
let id = epaint::TextureId::User(self.next_user_texture_id);
self.textures.insert(id, (None, bind_group));
self.next_user_texture_id += 1;
id
}
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to reuse `TextureId`s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
id: epaint::TextureId,
) {
crate::profile_function!();
let (_user_texture, user_texture_binding) = self
.textures
.get_mut(&id)
.expect("Tried to update a texture that has not been allocated yet.");
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
*user_texture_binding = bind_group;
}
/// Uploads the uniform, vertex and index data used by the renderer.
/// Should be called before `render()`.
///
/// Returns all user-defined command buffers gathered from prepare callbacks.
pub fn update_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
paint_jobs: &[epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
) -> Vec<wgpu::CommandBuffer> {
crate::profile_function!();
let screen_size_in_points = screen_descriptor.screen_size_in_points();
{
crate::profile_scope!("uniforms");
// Update uniform buffer
queue.write_buffer(
&self.uniform_buffer,
0,
bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points,
_padding: Default::default(),
}]),
);
}
// Determine how many vertices & indices need to be rendered.
let (vertex_count, index_count) = {
crate::profile_scope!("count_vertices_indices");
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
match &clipped_primitive.primitive {
Primitive::Mesh(mesh) => {
(acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
}
Primitive::Callback(_) => acc,
}
})
};
{
// Resize index buffer if needed:
self.index_buffer.slices.clear();
let required_size = (std::mem::size_of::<u32>() * index_count) as u64;
if self.index_buffer.capacity < required_size {
self.index_buffer.capacity =
(self.index_buffer.capacity * 2).at_least(required_size);
self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
}
}
{
// Resize vertex buffer if needed:
self.vertex_buffer.slices.clear();
let required_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
if self.vertex_buffer.capacity < required_size {
self.vertex_buffer.capacity =
(self.vertex_buffer.capacity * 2).at_least(required_size);
self.vertex_buffer.buffer =
create_vertex_buffer(device, self.vertex_buffer.capacity);
}
}
// Upload index & vertex data and call user callbacks
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
crate::profile_scope!("primitives");
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
match primitive {
Primitive::Mesh(mesh) => {
{
let index_offset = self.index_buffer.slices.last().unwrap_or(&(0..0)).end;
let data = bytemuck::cast_slice(&mesh.indices);
queue.write_buffer(&self.index_buffer.buffer, index_offset, data);
self.index_buffer
.slices
.push(index_offset..(data.len() as wgpu::BufferAddress + index_offset));
}
{
let vertex_offset = self.vertex_buffer.slices.last().unwrap_or(&(0..0)).end;
let data = bytemuck::cast_slice(&mesh.vertices);
queue.write_buffer(&self.vertex_buffer.buffer, vertex_offset, data);
self.vertex_buffer.slices.push(
vertex_offset..(data.len() as wgpu::BufferAddress + vertex_offset),
);
}
}
Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c
} else {
tracing::warn!("Unknown paint callback: expected `egui_wgpu::CallbackFn`");
continue;
};
crate::profile_scope!("callback");
user_cmd_bufs.extend((cbfn.prepare)(
device,
queue,
encoder,
&mut self.paint_callback_resources,
));
}
}
}
user_cmd_bufs
}
}
fn create_sampler(
options: epaint::textures::TextureOptions,
device: &wgpu::Device,
) -> wgpu::Sampler {
let mag_filter = match options.magnification {
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
let min_filter = match options.minification {
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!(
"egui sampler (mag: {:?}, min {:?})",
mag_filter, min_filter
)),
mag_filter,
min_filter,
..Default::default()
})
}
fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
crate::profile_function!();
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_vertex_buffer"),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
size,
mapped_at_creation: false,
})
}
fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
crate::profile_function!();
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_index_buffer"),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
size,
mapped_at_creation: false,
})
}
/// A Rect in physical pixel space, used for setting cliipping rectangles.
struct ScissorRect {
x: u32,
y: u32,
width: u32,
height: u32,
}
impl ScissorRect {
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
// Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y;
let clip_max_x = pixels_per_point * clip_rect.max.x;
let clip_max_y = pixels_per_point * clip_rect.max.y;
// Round to integer:
let clip_min_x = clip_min_x.round() as u32;
let clip_min_y = clip_min_y.round() as u32;
let clip_max_x = clip_max_x.round() as u32;
let clip_max_y = clip_max_y.round() as u32;
// Clamp:
let clip_min_x = clip_min_x.clamp(0, target_size[0]);
let clip_min_y = clip_min_y.clamp(0, target_size[1]);
let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
Self {
x: clip_min_x,
y: clip_min_y,
width: clip_max_x - clip_min_x,
height: clip_max_y - clip_min_y,
}
}
}
#[test]
fn renderer_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Renderer>();
}

View file

@ -1,418 +0,0 @@
use std::sync::Arc;
use epaint::mutex::RwLock;
use tracing::error;
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
struct SurfaceState {
surface: wgpu::Surface,
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
///
/// Alternatively you can use [`crate::renderer`] directly.
pub struct Painter {
configuration: WgpuConfiguration,
msaa_samples: u32,
support_transparent_backbuffer: bool,
depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
instance: wgpu::Instance,
adapter: Option<wgpu::Adapter>,
render_state: Option<RenderState>,
surface_state: Option<SurfaceState>,
}
impl Painter {
/// Manages [`wgpu`] state, including surface state, required to render egui.
///
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
/// of render + surface state is deferred until the painter is given its first window target
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
/// native window is chosen)
///
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
/// [`set_window()`](Self::set_window) once you have
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
/// associated.
pub fn new(
configuration: WgpuConfiguration,
msaa_samples: u32,
depth_bits: u8,
support_transparent_backbuffer: bool,
) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: configuration.backends,
dx12_shader_compiler: Default::default(), //
});
Self {
configuration,
msaa_samples,
support_transparent_backbuffer,
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
depth_texture_view: None,
instance,
adapter: None,
render_state: None,
surface_state: None,
}
}
/// Get the [`RenderState`].
///
/// Will return [`None`] if the render state has not been initialized yet.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
async fn init_render_state(
&self,
adapter: &wgpu::Adapter,
target_format: wgpu::TextureFormat,
) -> Result<RenderState, wgpu::RequestDeviceError> {
adapter
.request_device(&self.configuration.device_descriptor, None)
.await
.map(|(device, queue)| {
let renderer =
Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
RenderState {
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
renderer: Arc::new(RwLock::new(renderer)),
}
})
}
// We want to defer the initialization of our render state until we have a surface
// so we can take its format into account.
//
// After we've initialized our render state once though we expect all future surfaces
// will have the same format and so this render state will remain valid.
async fn ensure_render_state_for_surface(
&mut self,
surface: &wgpu::Surface,
) -> Result<(), wgpu::RequestDeviceError> {
if self.adapter.is_none() {
self.adapter = self
.instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: self.configuration.power_preference,
compatible_surface: Some(surface),
force_fallback_adapter: false,
})
.await;
}
if self.render_state.is_none() {
match &self.adapter {
Some(adapter) => {
let swapchain_format = crate::preferred_framebuffer_format(
&surface.get_capabilities(adapter).formats,
);
let rs = self.init_render_state(adapter, swapchain_format).await?;
self.render_state = Some(rs);
}
None => return Err(wgpu::RequestDeviceError {}),
}
}
Ok(())
}
fn configure_surface(
surface_state: &SurfaceState,
render_state: &RenderState,
present_mode: wgpu::PresentMode,
) {
surface_state.surface.configure(
&render_state.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format,
width: surface_state.width,
height: surface_state.height,
present_mode,
alpha_mode: surface_state.alpha_mode,
view_formats: vec![render_state.target_format],
},
);
}
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
///
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
/// state if needed) that is used for egui rendering.
///
/// This must be called before trying to render via
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
///
/// # Portability
///
/// _In particular it's important to note that on Android a it's only possible to create
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
/// attempts to query the raw window handle while paused._
///
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
/// valid [`winit::window::Window`].
///
/// # Safety
///
/// The raw Window handle associated with the given `window` must be a valid object to create a
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
/// be cleared by passing `None`).
///
/// # Errors
/// If the provided wgpu configuration does not match an available device.
pub async unsafe fn set_window(
&mut self,
window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> {
match window {
Some(window) => {
let surface = self.instance.create_surface(&window)?;
self.ensure_render_state_for_surface(&surface).await?;
let alpha_mode = if self.support_transparent_backbuffer {
let supported_alpha_modes = surface
.get_capabilities(self.adapter.as_ref().unwrap())
.alpha_modes;
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else if supported_alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else {
tracing::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
wgpu::CompositeAlphaMode::Auto
}
} else {
wgpu::CompositeAlphaMode::Auto
};
let size = window.inner_size();
self.surface_state = Some(SurfaceState {
surface,
width: size.width,
height: size.height,
alpha_mode,
});
self.resize_and_generate_depth_texture_view(size.width, size.height);
}
None => {
self.surface_state = None;
}
}
Ok(())
}
/// Returns the maximum texture dimension supported if known
///
/// This API will only return a known dimension after `set_window()` has been called
/// at least once, since the underlying device and render state are initialized lazily
/// once we have a window (that may determine the choice of adapter/device).
pub fn max_texture_side(&self) -> Option<usize> {
self.render_state
.as_ref()
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
}
fn resize_and_generate_depth_texture_view(
&mut self,
width_in_pixels: u32,
height_in_pixels: u32,
) {
let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surface_state.as_mut().unwrap();
surface_state.width = width_in_pixels;
surface_state.height = height_in_pixels;
Self::configure_surface(surface_state, render_state, self.configuration.present_mode);
self.depth_texture_view = self.depth_format.map(|depth_format| {
render_state
.device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
size: wgpu::Extent3d {
width: width_in_pixels,
height: height_in_pixels,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[depth_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
});
}
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
if self.surface_state.is_some() {
self.resize_and_generate_depth_texture_view(width_in_pixels, height_in_pixels);
} else {
error!("Ignoring window resize notification with no surface created via Painter::set_window()");
}
}
pub fn paint_and_update_textures(
&mut self,
pixels_per_point: f32,
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
) {
crate::profile_function!();
let render_state = match self.render_state.as_mut() {
Some(rs) => rs,
None => return,
};
let surface_state = match self.surface_state.as_ref() {
Some(rs) => rs,
None => return,
};
let output_frame = {
crate::profile_scope!("get_current_texture");
// This is what vsync-waiting happens, at least on Mac.
surface_state.surface.get_current_texture()
};
let output_frame = match output_frame {
Ok(frame) => frame,
#[allow(clippy::single_match_else)]
Err(e) => match (*self.configuration.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(
surface_state,
render_state,
self.configuration.present_mode,
);
return;
}
SurfaceErrorAction::SkipFrame => {
return;
}
},
};
let mut encoder =
render_state
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("encoder"),
});
// Upload all resources for the GPU.
let screen_descriptor = renderer::ScreenDescriptor {
size_in_pixels: [surface_state.width, surface_state.height],
pixels_per_point,
};
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
renderer.update_buffers(
&render_state.device,
&render_state.queue,
&mut encoder,
clipped_primitives,
&screen_descriptor,
)
};
{
let renderer = render_state.renderer.read();
let frame_view = output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear_color[0] as f64,
g: clear_color[1] as f64,
b: clear_color[2] as f64,
a: clear_color[3] as f64,
}),
store: true,
},
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: true,
}),
stencil_ops: None,
}
}),
label: Some("egui_render"),
});
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
}
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
let encoded = {
crate::profile_scope!("CommandEncoder::finish");
encoder.finish()
};
// Submit the commands: both the main buffer and user-defined ones.
{
crate::profile_scope!("Queue::submit");
render_state
.queue
.submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded)));
};
// Redraw egui
{
crate::profile_scope!("present");
output_frame.present();
}
}
#[allow(clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}
}

View file

@ -1,64 +0,0 @@
# Changelog for egui-winit
All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
## 0.21.1 - 2023-02-12
* 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)).
## 0.21.0 - 2023-02-08
* Fixed persistence of native window position on Windows OS ([#2583](https://github.com/emilk/egui/issues/2583)).
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
* Fix bug where the cursor could get stuck using the wrong icon.
## 0.20.1 - 2022-12-11
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
## 0.20.0 - 2022-12-08
* 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)).
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* Fixed clipboard on Wayland ([#1613](https://github.com/emilk/egui/pull/1613)).
* Allow deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
* Fixed window position persistence ([#1745](https://github.com/emilk/egui/pull/1745)).
* Fixed mouse cursor change on Linux ([#1747](https://github.com/emilk/egui/pull/1747)).
* Use the new `RawInput::has_focus` field to indicate whether the window has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
## 0.18.0 - 2022-04-30
* Reexport `egui` crate
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Added new feature `puffin` to add [`puffin profiler`](https://github.com/EmbarkStudios/puffin) scopes ([#1483](https://github.com/emilk/egui/pull/1483)).
* Renamed the feature `convert_bytemuck` to `bytemuck` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed the feature `serialize` to `serde` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Removed the features `dark-light` and `persistence` ([#1542](https://github.com/emilk/egui/pull/1542)).
## 0.17.0 - 2022-02-22
* Fixed horizontal scrolling direction on Linux.
* Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatability ([#1023](https://github.com/emilk/egui/pull/1023))
* Automatically detect and apply dark or light mode from system ([#1045](https://github.com/emilk/egui/pull/1045)).
* Fixed `enable_drag` on Windows OS ([#1108](https://github.com/emilk/egui/pull/1108)).
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)).
## 0.16.0 - 2021-12-29
* Added helper `EpiIntegration` ([#871](https://github.com/emilk/egui/pull/871)).
* Fixed shift key getting stuck enabled with the X11 option `shift:both_capslock` enabled ([#849](https://github.com/emilk/egui/pull/849)).
* Removed `State::is_quit_event` and `State::is_quit_shortcut` ([#881](https://github.com/emilk/egui/pull/881)).
* Updated `winit` to 0.26 ([#930](https://github.com/emilk/egui/pull/930)).
## 0.15.0 - 2021-10-24
First stand-alone release. Previously part of `egui_glium`.

View file

@ -1,76 +0,0 @@
[package]
name = "egui-winit"
version = "0.21.1"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
[features]
default = ["clipboard", "links", "wayland", "winit/default"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"]
## Enable cut/copy/paste to OS clipboard.
## If disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["arboard", "smithay-clipboard"]
## Enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde"]
## Enables Wayland support.
wayland = ["winit/wayland"]
[dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
"tracing",
] }
instant = { version = "0.1", features = [
"wasm-bindgen",
] } # We use instant so we can (maybe) compile for web
tracing = { version = "0.1", default-features = false, features = ["std"] }
winit = { version = "0.28", default-features = false }
#! ### Optional dependencies
# feature accesskit
accesskit_winit = { version = "0.10.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
puffin = { version = "0.14", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.8.3", optional = true }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.6.3", optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.2", optional = true, default-features = false }
[target.'cfg(target_os = "android")'.dependencies]
# TODO(emilk): this is probably not the right place for specifying native-activity, but we need to do it somewhere for the CI
android-activity = { version = "0.4", features = ["native-activity"] }

View file

@ -1,11 +0,0 @@
# egui-winit
[![Latest version](https://img.shields.io/crates/v/egui-winit.svg)](https://crates.io/crates/egui-winit)
[![Documentation](https://docs.rs/egui-winit/badge.svg)](https://docs.rs/egui-winit)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [`winit`](https://crates.io/crates/winit).
The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc.

View file

@ -1,146 +0,0 @@
use std::os::raw::c_void;
/// Handles interfacing with the OS clipboard.
///
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works works within the same app is used instead.
pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: Option<arboard::Clipboard>,
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: Option<smithay_clipboard::Clipboard>,
/// Fallback manual clipboard.
clipboard: String,
}
impl Clipboard {
#[allow(unused_variables)]
pub fn new(#[allow(unused_variables)] wayland_display: Option<*mut c_void>) -> Self {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(),
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
smithay: init_smithay_clipboard(wayland_display),
clipboard: Default::default(),
}
}
pub fn get(&mut self) -> Option<String> {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
return match clipboard.load() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("smithay paste error: {err}");
None
}
};
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("arboard paste error: {err}");
None
}
};
}
Some(self.clipboard.clone())
}
pub fn set(&mut self, text: String) {
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
clipboard.store(text);
return;
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
tracing::error!("arboard copy/cut error: {err}");
}
return;
}
self.clipboard = text;
}
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> {
tracing::debug!("Initializing arboard clipboard…");
match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(err) => {
tracing::warn!("Failed to initialize arboard clipboard: {err}");
None
}
}
}
#[cfg(all(
any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "smithay-clipboard"
))]
fn init_smithay_clipboard(
wayland_display: Option<*mut c_void>,
) -> Option<smithay_clipboard::Clipboard> {
if let Some(display) = wayland_display {
tracing::debug!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
} else {
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
None
}
}

View file

@ -1,920 +0,0 @@
//! [`egui`] bindings for [`winit`](https://github.com/rust-windowing/winit).
//!
//! The library translates winit events to egui, handled copy/paste,
//! updates the cursor, open links clicked in egui, etc.
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::manual_range_contains)]
use std::os::raw::c_void;
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
#[cfg(feature = "accesskit")]
use egui::accesskit;
pub use winit;
pub mod clipboard;
mod window_settings;
pub use window_settings::WindowSettings;
use winit::event_loop::EventLoopWindowTarget;
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
window.scale_factor() as f32
}
pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 {
let size = window.inner_size();
egui::vec2(size.width as f32, size.height as f32)
}
// ----------------------------------------------------------------------------
#[must_use]
pub struct EventResponse {
/// If true, egui consumed this event, i.e. wants exclusive use of this event
/// (e.g. a mouse click on an egui window, or entering text into a text field).
///
/// For instance, if you use egui for a game, you should only
/// pass on the events to your game when [`Self::consumed`] is `false.
///
/// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs.
pub consumed: bool,
/// Do we need an egui refresh because of this event?
pub repaint: bool,
}
// ----------------------------------------------------------------------------
/// Handles the integration between egui and winit.
pub struct State {
start_time: instant::Instant,
egui_input: egui::RawInput,
pointer_pos_in_points: Option<egui::Pos2>,
any_pointer_button_down: bool,
current_cursor_icon: Option<egui::CursorIcon>,
/// What egui uses.
current_pixels_per_point: f32,
clipboard: clipboard::Clipboard,
/// If `true`, mouse inputs will be treated as touches.
/// Useful for debugging touch support in egui.
///
/// Creates duplicate touches, if real touch inputs are coming.
simulate_touch_screen: bool,
/// Is Some(…) when a touch is being translated to a pointer.
///
/// Only one touch will be interpreted as pointer at any time.
pointer_touch_id: Option<u64>,
/// track ime state
input_method_editor_started: bool,
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
}
impl State {
pub fn new<T>(event_loop: &EventLoopWindowTarget<T>) -> Self {
Self::new_with_wayland_display(wayland_display(event_loop))
}
pub fn new_with_wayland_display(wayland_display: Option<*mut c_void>) -> Self {
let egui_input = egui::RawInput {
has_focus: false, // winit will tell us when we have focus
..Default::default()
};
Self {
start_time: instant::Instant::now(),
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
current_pixels_per_point: 1.0,
clipboard: clipboard::Clipboard::new(wayland_display),
simulate_touch_screen: false,
pointer_touch_id: None,
input_method_editor_started: false,
#[cfg(feature = "accesskit")]
accesskit: None,
}
}
#[cfg(feature = "accesskit")]
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
&mut self,
window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
) {
self.accesskit = Some(accesskit_winit::Adapter::new(
window,
initial_tree_update_factory,
event_loop_proxy,
));
}
/// Call this once a graphics context has been created to update the maximum texture dimensions
/// that egui will use.
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
self.egui_input.max_texture_side = Some(max_texture_side);
}
/// Call this when a new native Window is created for rendering to initialize the `pixels_per_point`
/// for that window.
///
/// In particular, on Android it is necessary to call this after each `Resumed` lifecycle
/// event, each time a new native window is created.
///
/// Once this has been initialized for a new window then this state will be maintained by handling
/// [`winit::event::WindowEvent::ScaleFactorChanged`] events.
pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
self.egui_input.pixels_per_point = Some(pixels_per_point);
self.current_pixels_per_point = pixels_per_point;
}
/// The number of physical pixels per logical point,
/// as configured on the current egui context (see [`egui::Context::pixels_per_point`]).
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.current_pixels_per_point
}
/// The current input state.
/// This is changed by [`Self::on_event`] and cleared by [`Self::take_egui_input`].
#[inline]
pub fn egui_input(&self) -> &egui::RawInput {
&self.egui_input
}
/// Prepare for a new frame by extracting the accumulated input,
/// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect).
pub fn take_egui_input(&mut self, window: &winit::window::Window) -> egui::RawInput {
let pixels_per_point = self.pixels_per_point();
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
// On Windows, a minimized window will have 0 width and height.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where egui window positions would be changed when minimizing on Windows.
let screen_size_in_pixels = screen_size_in_pixels(window);
let screen_size_in_points = screen_size_in_pixels / pixels_per_point;
self.egui_input.screen_rect =
if screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0 {
Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
screen_size_in_points,
))
} else {
None
};
self.egui_input.take()
}
/// Call this when there is a new event.
///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
pub fn on_event(
&mut self,
egui_ctx: &egui::Context,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
use winit::event::WindowEvent;
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let pixels_per_point = *scale_factor as f32;
self.egui_input.pixels_per_point = Some(pixels_per_point);
self.current_pixels_per_point = pixels_per_point;
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::MouseInput { state, button, .. } => {
self.on_mouse_button_input(*state, *button);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(*delta);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
WindowEvent::CursorMoved { position, .. } => {
self.on_cursor_moved(*position);
EventResponse {
repaint: true,
consumed: egui_ctx.is_using_pointer(),
}
}
WindowEvent::CursorLeft { .. } => {
self.pointer_pos_in_points = None;
self.egui_input.events.push(egui::Event::PointerGone);
EventResponse {
repaint: true,
consumed: false,
}
}
// WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO
WindowEvent::Touch(touch) => {
self.on_touch(touch);
let consumed = match touch.phase {
winit::event::TouchPhase::Started
| winit::event::TouchPhase::Ended
| winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(),
winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(),
};
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::ReceivedCharacter(ch) => {
// On Mac we get here when the user presses Cmd-C (copy), ctrl-W, etc.
// We need to ignore these characters that are side-effects of commands.
let is_mac_cmd = cfg!(target_os = "macos")
&& (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd);
let consumed = if is_printable_char(*ch) && !is_mac_cmd {
self.egui_input
.events
.push(egui::Event::Text(ch.to_string()));
egui_ctx.wants_keyboard_input()
} else {
false
};
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::Ime(ime) => {
// on Mac even Cmd-C is preessed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
// How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS
// and Windows.
//
// - On Windows, before and after each Commit will produce an Enable/Disabled
// event.
// - On MacOS, only when user explicit enable/disable ime. No Disabled
// after Commit.
//
// We use input_method_editor_started to mannualy insert CompositionStart
// between Commits.
match ime {
winit::event::Ime::Enabled | winit::event::Ime::Disabled => (),
winit::event::Ime::Commit(text) => {
self.input_method_editor_started = false;
self.egui_input
.events
.push(egui::Event::CompositionEnd(text.clone()));
}
winit::event::Ime::Preedit(text, ..) => {
if !self.input_method_editor_started {
self.input_method_editor_started = true;
self.egui_input.events.push(egui::Event::CompositionStart);
}
self.egui_input
.events
.push(egui::Event::CompositionUpdate(text.clone()));
}
};
EventResponse {
repaint: true,
consumed: egui_ctx.wants_keyboard_input(),
}
}
WindowEvent::KeyboardInput { input, .. } => {
self.on_keyboard_input(input);
let consumed = egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
EventResponse {
repaint: true,
consumed,
}
}
WindowEvent::Focused(has_focus) => {
self.egui_input.has_focus = *has_focus;
// We will not be given a KeyboardInput event when the modifiers are released while
// the window does not have focus. Unset all modifier state to be safe.
self.egui_input.modifiers = egui::Modifiers::default();
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::HoveredFile(path) => {
self.egui_input.hovered_files.push(egui::HoveredFile {
path: Some(path.clone()),
..Default::default()
});
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::HoveredFileCancelled => {
self.egui_input.hovered_files.clear();
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::DroppedFile(path) => {
self.egui_input.hovered_files.clear();
self.egui_input.dropped_files.push(egui::DroppedFile {
path: Some(path.clone()),
..Default::default()
});
EventResponse {
repaint: true,
consumed: false,
}
}
WindowEvent::ModifiersChanged(state) => {
self.egui_input.modifiers.alt = state.alt();
self.egui_input.modifiers.ctrl = state.ctrl();
self.egui_input.modifiers.shift = state.shift();
self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && state.logo();
self.egui_input.modifiers.command = if cfg!(target_os = "macos") {
state.logo()
} else {
state.ctrl()
};
EventResponse {
repaint: true,
consumed: false,
}
}
// Things that may require repaint:
WindowEvent::CloseRequested
| WindowEvent::CursorEntered { .. }
| WindowEvent::Destroyed
| WindowEvent::Occluded(_)
| WindowEvent::Resized(_)
| WindowEvent::ThemeChanged(_)
| WindowEvent::TouchpadPressure { .. } => EventResponse {
repaint: true,
consumed: false,
},
// Things we completely ignore:
WindowEvent::AxisMotion { .. }
| WindowEvent::Moved(_)
| WindowEvent::SmartMagnify { .. }
| WindowEvent::TouchpadRotate { .. } => EventResponse {
repaint: false,
consumed: false,
},
WindowEvent::TouchpadMagnify { delta, .. } => {
// Positive delta values indicate magnification (zooming in).
// Negative delta values indicate shrinking (zooming out).
let zoom_factor = (*delta as f32).exp();
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
}
}
}
/// Call this when there is a new [`accesskit::ActionRequest`].
///
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
#[cfg(feature = "accesskit")]
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
self.egui_input
.events
.push(egui::Event::AccessKitActionRequest(request));
}
fn on_mouse_button_input(
&mut self,
state: winit::event::ElementState,
button: winit::event::MouseButton,
) {
if let Some(pos) = self.pointer_pos_in_points {
if let Some(button) = translate_mouse_button(button) {
let pressed = state == winit::event::ElementState::Pressed;
self.egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
});
if self.simulate_touch_screen {
if pressed {
self.any_pointer_button_down = true;
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: 0.0,
});
} else {
self.any_pointer_button_down = false;
self.egui_input.events.push(egui::Event::PointerGone);
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: 0.0,
});
};
}
}
}
}
fn on_cursor_moved(&mut self, pos_in_pixels: winit::dpi::PhysicalPosition<f64>) {
let pos_in_points = egui::pos2(
pos_in_pixels.x as f32 / self.pixels_per_point(),
pos_in_pixels.y as f32 / self.pixels_per_point(),
);
self.pointer_pos_in_points = Some(pos_in_points);
if self.simulate_touch_screen {
if self.any_pointer_button_down {
self.egui_input
.events
.push(egui::Event::PointerMoved(pos_in_points));
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Move,
pos: pos_in_points,
force: 0.0,
});
}
} else {
self.egui_input
.events
.push(egui::Event::PointerMoved(pos_in_points));
}
}
fn on_touch(&mut self, touch: &winit::event::Touch) {
// Emit touch event
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
id: egui::TouchId::from(touch.id),
phase: match touch.phase {
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
},
pos: egui::pos2(
touch.location.x as f32 / self.pixels_per_point(),
touch.location.y as f32 / self.pixels_per_point(),
),
force: match touch.force {
Some(winit::event::Force::Normalized(force)) => force as f32,
Some(winit::event::Force::Calibrated {
force,
max_possible_force,
..
}) => (force / max_possible_force) as f32,
None => 0_f32,
},
});
// If we're not yet tanslating a touch or we're translating this very
// touch …
if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap() == touch.id {
// … emit PointerButton resp. PointerMoved events to emulate mouse
match touch.phase {
winit::event::TouchPhase::Started => {
self.pointer_touch_id = Some(touch.id);
// First move the pointer to the right location
self.on_cursor_moved(touch.location);
self.on_mouse_button_input(
winit::event::ElementState::Pressed,
winit::event::MouseButton::Left,
);
}
winit::event::TouchPhase::Moved => {
self.on_cursor_moved(touch.location);
}
winit::event::TouchPhase::Ended => {
self.pointer_touch_id = None;
self.on_mouse_button_input(
winit::event::ElementState::Released,
winit::event::MouseButton::Left,
);
// The pointer should vanish completely to not get any
// hover effects
self.pointer_pos_in_points = None;
self.egui_input.events.push(egui::Event::PointerGone);
}
winit::event::TouchPhase::Cancelled => {
self.pointer_touch_id = None;
self.pointer_pos_in_points = None;
self.egui_input.events.push(egui::Event::PointerGone);
}
}
}
}
fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
let delta = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
egui::vec2(x, y) * points_per_scroll_line
}
winit::event::MouseScrollDelta::PixelDelta(delta) => {
egui::vec2(delta.x as f32, delta.y as f32) / self.pixels_per_point()
}
};
if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command {
// Treat as zoom instead:
let factor = (delta.y / 200.0).exp();
self.egui_input.events.push(egui::Event::Zoom(factor));
} else if self.egui_input.modifiers.shift {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
self.egui_input
.events
.push(egui::Event::Scroll(egui::vec2(delta.x + delta.y, 0.0)));
} else {
self.egui_input.events.push(egui::Event::Scroll(delta));
}
}
fn on_keyboard_input(&mut self, input: &winit::event::KeyboardInput) {
if let Some(keycode) = input.virtual_keycode {
let pressed = input.state == winit::event::ElementState::Pressed;
if pressed {
// VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
// so we detect these things manually:
if is_cut_command(self.egui_input.modifiers, keycode) {
self.egui_input.events.push(egui::Event::Cut);
} else if is_copy_command(self.egui_input.modifiers, keycode) {
self.egui_input.events.push(egui::Event::Copy);
} else if is_paste_command(self.egui_input.modifiers, keycode) {
if let Some(contents) = self.clipboard.get() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
self.egui_input.events.push(egui::Event::Paste(contents));
}
}
}
}
if let Some(key) = translate_virtual_key_code(keycode) {
self.egui_input.events.push(egui::Event::Key {
key,
pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers,
});
}
}
}
/// Call with the output given by `egui`.
///
/// This will, if needed:
/// * update the cursor
/// * copy text to the clipboard
/// * open any clicked urls
/// * update the IME
/// *
pub fn handle_platform_output(
&mut self,
window: &winit::window::Window,
egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput,
) {
let egui::PlatformOutput {
cursor_icon,
open_url,
copied_text,
events: _, // handled above
mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = platform_output;
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
}
if !copied_text.is_empty() {
self.clipboard.set(copied_text);
}
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
}
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_ref() {
if let Some(update) = accesskit_update {
accesskit.update_if_active(|| update);
}
}
}
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
if self.current_cursor_icon == Some(cursor_icon) {
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
// On other platforms: just early-out to save CPU.
return;
}
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window {
self.current_cursor_icon = Some(cursor_icon);
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor_icon(winit_cursor_icon);
} else {
window.set_cursor_visible(false);
}
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
}
}
}
fn open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) {
tracing::warn!("Failed to open url: {}", err);
}
#[cfg(not(feature = "webbrowser"))]
{
tracing::warn!("Cannot open url - feature \"links\" not enabled.");
}
}
/// Winit sends special keys (backspace, delete, F1, …) as characters.
/// Ignore those.
/// We also ignore '\r', '\n', '\t'.
/// Newlines are handled by the `Key::Enter` event.
fn is_printable_char(chr: char) -> bool {
let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}'
|| '\u{f0000}' <= chr && chr <= '\u{ffffd}'
|| '\u{100000}' <= chr && chr <= '\u{10fffd}';
!is_in_private_use_area && !chr.is_ascii_control()
}
fn is_cut_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::X)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == winit::event::VirtualKeyCode::Delete)
}
fn is_copy_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::C)
|| (cfg!(target_os = "windows")
&& modifiers.ctrl
&& keycode == winit::event::VirtualKeyCode::Insert)
}
fn is_paste_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool {
(modifiers.command && keycode == winit::event::VirtualKeyCode::V)
|| (cfg!(target_os = "windows")
&& modifiers.shift
&& keycode == winit::event::VirtualKeyCode::Insert)
}
fn translate_mouse_button(button: winit::event::MouseButton) -> Option<egui::PointerButton> {
match button {
winit::event::MouseButton::Left => Some(egui::PointerButton::Primary),
winit::event::MouseButton::Right => Some(egui::PointerButton::Secondary),
winit::event::MouseButton::Middle => Some(egui::PointerButton::Middle),
winit::event::MouseButton::Other(1) => Some(egui::PointerButton::Extra1),
winit::event::MouseButton::Other(2) => Some(egui::PointerButton::Extra2),
winit::event::MouseButton::Other(_) => None,
}
}
fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui::Key> {
use egui::Key;
use winit::event::VirtualKeyCode;
Some(match key {
VirtualKeyCode::Down => Key::ArrowDown,
VirtualKeyCode::Left => Key::ArrowLeft,
VirtualKeyCode::Right => Key::ArrowRight,
VirtualKeyCode::Up => Key::ArrowUp,
VirtualKeyCode::Escape => Key::Escape,
VirtualKeyCode::Tab => Key::Tab,
VirtualKeyCode::Back => Key::Backspace,
VirtualKeyCode::Return => Key::Enter,
VirtualKeyCode::Space => Key::Space,
VirtualKeyCode::Insert => Key::Insert,
VirtualKeyCode::Delete => Key::Delete,
VirtualKeyCode::Home => Key::Home,
VirtualKeyCode::End => Key::End,
VirtualKeyCode::PageUp => Key::PageUp,
VirtualKeyCode::PageDown => Key::PageDown,
VirtualKeyCode::Minus => Key::Minus,
// Using Mac the key with the Plus sign on it is reported as the Equals key
// (with both English and Swedish keyboard).
VirtualKeyCode::Equals => Key::PlusEquals,
VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0,
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1,
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2,
VirtualKeyCode::Key3 | VirtualKeyCode::Numpad3 => Key::Num3,
VirtualKeyCode::Key4 | VirtualKeyCode::Numpad4 => Key::Num4,
VirtualKeyCode::Key5 | VirtualKeyCode::Numpad5 => Key::Num5,
VirtualKeyCode::Key6 | VirtualKeyCode::Numpad6 => Key::Num6,
VirtualKeyCode::Key7 | VirtualKeyCode::Numpad7 => Key::Num7,
VirtualKeyCode::Key8 | VirtualKeyCode::Numpad8 => Key::Num8,
VirtualKeyCode::Key9 | VirtualKeyCode::Numpad9 => Key::Num9,
VirtualKeyCode::A => Key::A,
VirtualKeyCode::B => Key::B,
VirtualKeyCode::C => Key::C,
VirtualKeyCode::D => Key::D,
VirtualKeyCode::E => Key::E,
VirtualKeyCode::F => Key::F,
VirtualKeyCode::G => Key::G,
VirtualKeyCode::H => Key::H,
VirtualKeyCode::I => Key::I,
VirtualKeyCode::J => Key::J,
VirtualKeyCode::K => Key::K,
VirtualKeyCode::L => Key::L,
VirtualKeyCode::M => Key::M,
VirtualKeyCode::N => Key::N,
VirtualKeyCode::O => Key::O,
VirtualKeyCode::P => Key::P,
VirtualKeyCode::Q => Key::Q,
VirtualKeyCode::R => Key::R,
VirtualKeyCode::S => Key::S,
VirtualKeyCode::T => Key::T,
VirtualKeyCode::U => Key::U,
VirtualKeyCode::V => Key::V,
VirtualKeyCode::W => Key::W,
VirtualKeyCode::X => Key::X,
VirtualKeyCode::Y => Key::Y,
VirtualKeyCode::Z => Key::Z,
VirtualKeyCode::F1 => Key::F1,
VirtualKeyCode::F2 => Key::F2,
VirtualKeyCode::F3 => Key::F3,
VirtualKeyCode::F4 => Key::F4,
VirtualKeyCode::F5 => Key::F5,
VirtualKeyCode::F6 => Key::F6,
VirtualKeyCode::F7 => Key::F7,
VirtualKeyCode::F8 => Key::F8,
VirtualKeyCode::F9 => Key::F9,
VirtualKeyCode::F10 => Key::F10,
VirtualKeyCode::F11 => Key::F11,
VirtualKeyCode::F12 => Key::F12,
VirtualKeyCode::F13 => Key::F13,
VirtualKeyCode::F14 => Key::F14,
VirtualKeyCode::F15 => Key::F15,
VirtualKeyCode::F16 => Key::F16,
VirtualKeyCode::F17 => Key::F17,
VirtualKeyCode::F18 => Key::F18,
VirtualKeyCode::F19 => Key::F19,
VirtualKeyCode::F20 => Key::F20,
_ => {
return None;
}
})
}
fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::CursorIcon> {
match cursor_icon {
egui::CursorIcon::None => None,
egui::CursorIcon::Alias => Some(winit::window::CursorIcon::Alias),
egui::CursorIcon::AllScroll => Some(winit::window::CursorIcon::AllScroll),
egui::CursorIcon::Cell => Some(winit::window::CursorIcon::Cell),
egui::CursorIcon::ContextMenu => Some(winit::window::CursorIcon::ContextMenu),
egui::CursorIcon::Copy => Some(winit::window::CursorIcon::Copy),
egui::CursorIcon::Crosshair => Some(winit::window::CursorIcon::Crosshair),
egui::CursorIcon::Default => Some(winit::window::CursorIcon::Default),
egui::CursorIcon::Grab => Some(winit::window::CursorIcon::Grab),
egui::CursorIcon::Grabbing => Some(winit::window::CursorIcon::Grabbing),
egui::CursorIcon::Help => Some(winit::window::CursorIcon::Help),
egui::CursorIcon::Move => Some(winit::window::CursorIcon::Move),
egui::CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop),
egui::CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed),
egui::CursorIcon::PointingHand => Some(winit::window::CursorIcon::Hand),
egui::CursorIcon::Progress => Some(winit::window::CursorIcon::Progress),
egui::CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize),
egui::CursorIcon::ResizeNeSw => Some(winit::window::CursorIcon::NeswResize),
egui::CursorIcon::ResizeNwSe => Some(winit::window::CursorIcon::NwseResize),
egui::CursorIcon::ResizeVertical => Some(winit::window::CursorIcon::NsResize),
egui::CursorIcon::ResizeEast => Some(winit::window::CursorIcon::EResize),
egui::CursorIcon::ResizeSouthEast => Some(winit::window::CursorIcon::SeResize),
egui::CursorIcon::ResizeSouth => Some(winit::window::CursorIcon::SResize),
egui::CursorIcon::ResizeSouthWest => Some(winit::window::CursorIcon::SwResize),
egui::CursorIcon::ResizeWest => Some(winit::window::CursorIcon::WResize),
egui::CursorIcon::ResizeNorthWest => Some(winit::window::CursorIcon::NwResize),
egui::CursorIcon::ResizeNorth => Some(winit::window::CursorIcon::NResize),
egui::CursorIcon::ResizeNorthEast => Some(winit::window::CursorIcon::NeResize),
egui::CursorIcon::ResizeColumn => Some(winit::window::CursorIcon::ColResize),
egui::CursorIcon::ResizeRow => Some(winit::window::CursorIcon::RowResize),
egui::CursorIcon::Text => Some(winit::window::CursorIcon::Text),
egui::CursorIcon::VerticalText => Some(winit::window::CursorIcon::VerticalText),
egui::CursorIcon::Wait => Some(winit::window::CursorIcon::Wait),
egui::CursorIcon::ZoomIn => Some(winit::window::CursorIcon::ZoomIn),
egui::CursorIcon::ZoomOut => Some(winit::window::CursorIcon::ZoomOut),
}
}
/// Returns a Wayland display handle if the target is running Wayland
fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> {
#[cfg(feature = "wayland")]
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
{
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
return _event_loop.wayland_display();
}
#[allow(unreachable_code)]
{
let _ = _event_loop;
None
}
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
#[allow(unused_imports)]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
#[allow(unused_imports)]
pub(crate) use profile_scope;

View file

@ -1,144 +0,0 @@
/// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WindowSettings {
/// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform.
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>,
fullscreen: bool,
/// Inner size of window in logical pixels
inner_size_points: Option<egui::Vec2>,
}
impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows.
window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else {
// Other platforms use the outer position.
window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};
Self {
position,
fullscreen: window.fullscreen().is_some(),
inner_size_points: Some(egui::vec2(
inner_size_points.width,
inner_size_points.height,
)),
}
}
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
self.inner_size_points
}
pub fn initialize_window(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the clamping behavior is managed by the function
// clamp_window_to_sane_position.
if let Some(pos) = self.position {
window = window.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
});
}
if let Some(inner_size_points) = self.inner_size_points {
window
.with_inner_size(winit::dpi::LogicalSize {
width: inner_size_points.x as f64,
height: inner_size_points.y as f64,
})
.with_fullscreen(
self.fullscreen
.then_some(winit::window::Fullscreen::Borderless(None)),
)
} else {
window
}
}
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
use egui::NumExt as _;
if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows
let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size);
*size = size.at_most(max_size);
}
}
pub fn clamp_window_to_sane_position<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
}
}
}

View file

@ -1,84 +0,0 @@
[package]
name = "egui"
version = "0.21.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021"
rust-version = "1.65"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "../../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[package.metadata.docs.rs]
all-features = true
[lib]
[features]
default = ["default_fonts"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
bytemuck = ["epaint/bytemuck"]
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
cint = ["epaint/cint"]
## Enable the [`hex_color`] macro.
color-hex = ["epaint/color-hex"]
## This will automatically detect deadlocks due to double-locking on the same thread.
## If your app freezes, you may want to enable this!
## Only affects [`epaint::mutex::RwLock`] (which egui uses a lot).
deadlock_detection = ["epaint/deadlock_detection"]
## If set, egui will use `include_bytes!` to bundle some fonts.
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
## Enable additional checks if debug assertions are enabled (debug builds).
extra_debug_asserts = ["epaint/extra_debug_asserts"]
## Always enable additional checks.
extra_asserts = ["epaint/extra_asserts"]
## [`mint`](https://docs.rs/mint) enables interopability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["epaint/mint"]
## Enable persistence of memory (window positions etc).
persistence = ["serde", "epaint/serde", "ron"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
[dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
ahash = { version = "0.8.1", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
] }
nohash-hasher = "0.2"
#! ### Optional dependencies
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = { version = "0.9.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
ron = { version = "0.8", optional = true }
serde = { version = "1", optional = true, features = ["derive", "rc"] }
# egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing).
tracing = { version = "0.1", optional = true, default-features = false, features = [
"std",
] }

View file

@ -1,7 +0,0 @@
There are no stand-alone egui examples, because egui is not stand-alone!
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!

View file

@ -1,114 +0,0 @@
use crate::{emath::remap_clamp, Id, IdMap, InputState};
#[derive(Clone, Default)]
pub(crate) struct AnimationManager {
bools: IdMap<BoolAnim>,
values: IdMap<ValueAnim>,
}
#[derive(Clone, Debug)]
struct BoolAnim {
value: bool,
/// when did `value` last toggle?
toggle_time: f64,
}
#[derive(Clone, Debug)]
struct ValueAnim {
from_value: f32,
to_value: f32,
/// when did `value` last toggle?
toggle_time: f64,
}
impl AnimationManager {
/// See `Context::animate_bool` for documentation
pub fn animate_bool(
&mut self,
input: &InputState,
animation_time: f32,
id: Id,
value: bool,
) -> f32 {
match self.bools.get_mut(&id) {
None => {
self.bools.insert(
id,
BoolAnim {
value,
toggle_time: -f64::INFINITY, // long time ago
},
);
if value {
1.0
} else {
0.0
}
}
Some(anim) => {
if anim.value != value {
anim.value = value;
anim.toggle_time = input.time;
}
let time_since_toggle = (input.time - anim.toggle_time) as f32;
// On the frame we toggle we don't want to return the old value,
// so we extrapolate forwards:
let time_since_toggle = time_since_toggle + input.predicted_dt;
if value {
remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0)
} else {
remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0)
}
}
}
}
pub fn animate_value(
&mut self,
input: &InputState,
animation_time: f32,
id: Id,
value: f32,
) -> f32 {
match self.values.get_mut(&id) {
None => {
self.values.insert(
id,
ValueAnim {
from_value: value,
to_value: value,
toggle_time: -f64::INFINITY, // long time ago
},
);
value
}
Some(anim) => {
let time_since_toggle = (input.time - anim.toggle_time) as f32;
// On the frame we toggle we don't want to return the old value,
// so we extrapolate forwards:
let time_since_toggle = time_since_toggle + input.predicted_dt;
let current_value = remap_clamp(
time_since_toggle,
0.0..=animation_time,
anim.from_value..=anim.to_value,
);
if anim.to_value != value {
anim.from_value = current_value; //start new animation from current position of playing animation
anim.to_value = value;
anim.toggle_time = input.time;
}
if animation_time == 0.0 {
anim.from_value = value;
anim.to_value = value;
}
current_value
}
}
}
}

View file

@ -1,514 +0,0 @@
//! Area is a [`Ui`] that has no parent, it floats on the background.
//! It has no frame or own size. It is potentially movable.
//! It is the foundation for windows and popups.
use crate::*;
/// State that is persisted between frames.
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State {
/// Last known pos of the pivot
pub pivot_pos: Pos2,
pub pivot: Align2,
/// Last know size. Used for catching clicks.
pub size: Vec2,
/// If false, clicks goes straight through to what is behind us.
/// Good for tooltips etc.
pub interactable: bool,
}
impl State {
pub fn left_top_pos(&self) -> Pos2 {
pos2(
self.pivot_pos.x - self.pivot.x().to_factor() * self.size.x,
self.pivot_pos.y - self.pivot.y().to_factor() * self.size.y,
)
}
pub fn set_left_top_pos(&mut self, pos: Pos2) {
self.pivot_pos = pos2(
pos.x + self.pivot.x().to_factor() * self.size.x,
pos.y + self.pivot.y().to_factor() * self.size.y,
);
}
pub fn rect(&self) -> Rect {
Rect::from_min_size(self.left_top_pos(), self.size)
}
}
/// An area on the screen that can be moved by dragging.
///
/// This forms the base of the [`Window`] container.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::Area::new("my_area")
/// .fixed_pos(egui::pos2(32.0, 32.0))
/// .show(ctx, |ui| {
/// ui.label("Floating text!");
/// });
/// # });
/// ```
#[must_use = "You should call .show()"]
#[derive(Clone, Copy, Debug)]
pub struct Area {
pub(crate) id: Id,
movable: bool,
interactable: bool,
enabled: bool,
constrain: bool,
order: Order,
default_pos: Option<Pos2>,
pivot: Align2,
anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>,
drag_bounds: Option<Rect>,
}
impl Area {
pub fn new(id: impl Into<Id>) -> Self {
Self {
id: id.into(),
movable: true,
interactable: true,
constrain: false,
enabled: true,
order: Order::Middle,
default_pos: None,
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
drag_bounds: None,
}
}
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
pub fn layer(&self) -> LayerId {
LayerId::new(self.order, self.id)
}
/// If false, no content responds to click
/// and widgets will be shown grayed out.
/// You won't be able to move the window.
/// Default: `true`.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// moveable by dragging the area?
pub fn movable(mut self, movable: bool) -> Self {
self.movable = movable;
self.interactable |= movable;
self
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn is_movable(&self) -> bool {
self.movable && self.enabled
}
/// If false, clicks goes straight through to what is behind us.
/// Good for tooltips etc.
pub fn interactable(mut self, interactable: bool) -> Self {
self.interactable = interactable;
self.movable &= interactable;
self
}
/// `order(Order::Foreground)` for an Area that should always be on top
pub fn order(mut self, order: Order) -> Self {
self.order = order;
self
}
pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
self.default_pos = Some(default_pos.into());
self
}
/// Positions the window and prevents it from being moved
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
self.new_pos = Some(fixed_pos.into());
self.movable = false;
self
}
/// Constrains this area to the screen bounds.
pub fn constrain(mut self, constrain: bool) -> Self {
self.constrain = constrain;
self
}
/// Where the "root" of the area is.
///
/// For instance, if you set this to [`Align2::RIGHT_TOP`]
/// then [`Self::fixed_pos`] will set the position of the right-top
/// corner of the area.
///
/// Default: [`Align2::LEFT_TOP`].
pub fn pivot(mut self, pivot: Align2) -> Self {
self.pivot = pivot;
self
}
/// Positions the window but you can still move it.
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
self.new_pos = Some(current_pos.into());
self
}
/// Set anchor and distance.
///
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
/// in the right-top corner of the screen".
///
/// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
/// would move the window left and down from the given anchor.
///
/// Anchoring also makes the window immovable.
///
/// It is an error to set both an anchor and a position.
pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
self.anchor = Some((align, offset.into()));
self.movable(false)
}
/// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.drag_bounds = Some(bounds);
self
}
pub(crate) fn get_pivot(&self) -> Align2 {
if let Some((pivot, _)) = self.anchor {
pivot
} else {
Align2::LEFT_TOP
}
}
}
pub(crate) struct Prepared {
layer_id: LayerId,
state: State,
move_response: Response,
enabled: bool,
drag_bounds: Option<Rect>,
/// We always make windows invisible the first frame to hide "first-frame-jitters".
///
/// This is so that we use the first frame to calculate the window size,
/// and then can correctly position the window and its contents the next frame,
/// without having one frame where the window is wrongly positioned or sized.
temporarily_invisible: bool,
}
impl Area {
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let prepared = self.begin(ctx);
let mut content_ui = prepared.content_ui(ctx);
let inner = add_contents(&mut content_ui);
let response = prepared.end(ctx, content_ui);
InnerResponse { inner, response }
}
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
let Area {
id,
movable,
order,
interactable,
enabled,
default_pos,
new_pos,
pivot,
anchor,
drag_bounds,
constrain,
} = self;
let layer_id = LayerId::new(order, id);
let state = ctx.memory(|mem| mem.areas.get(id).copied());
let is_new = state.is_none();
if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
}
let mut state = state.unwrap_or_else(|| State {
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
pivot,
size: Vec2::ZERO,
interactable,
});
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.interactable = interactable;
if let Some((anchor, offset)) = anchor {
let screen = ctx.available_rect();
state.set_left_top_pos(
anchor.align_size_within_rect(state.size, screen).left_top() + offset,
);
}
// interact right away to prevent frame-delay
let move_response = {
let interact_id = layer_id.id.with("move");
let sense = if movable {
Sense::click_and_drag()
} else if interactable {
Sense::click() // allow clicks to bring to front
} else {
Sense::hover()
};
let move_response = ctx.interact(
Rect::EVERYTHING,
ctx.style().spacing.item_spacing,
layer_id,
interact_id,
state.rect(),
sense,
enabled,
);
// Important check - don't try to move e.g. a combobox popup!
if movable {
if move_response.dragged() {
state.pivot_pos += ctx.input(|i| i.pointer.delta());
}
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.min,
);
}
if (move_response.dragged() || move_response.clicked())
|| pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
{
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
ctx.request_repaint();
}
move_response
};
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
if constrain {
state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
.left_top(),
);
}
Prepared {
layer_id,
state,
move_response,
enabled,
drag_bounds,
temporarily_invisible: is_new,
}
}
pub fn show_open_close_animation(&self, ctx: &Context, frame: &Frame, is_open: bool) {
// must be called first so animation managers know the latest state
let visibility_factor = ctx.animate_bool(self.id.with("close_animation"), is_open);
if is_open {
// we actually only show close animations.
// when opening a window we show it right away.
return;
}
if visibility_factor <= 0.0 {
return;
}
let layer_id = LayerId::new(self.order, self.id);
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
if let Some(area_rect) = area_rect {
let clip_rect = ctx.available_rect();
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
// shrinkage: looks kinda a bad on its own
// let area_rect =
// Rect::from_center_size(area_rect.center(), visibility_factor * area_rect.size());
let frame = frame.multiply_with_opacity(visibility_factor);
painter.add(frame.paint(area_rect));
}
}
}
impl Prepared {
pub(crate) fn state(&self) -> &State {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut State {
&mut self.state
}
pub(crate) fn drag_bounds(&self) -> Option<Rect> {
self.drag_bounds
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds
} else {
let central_area = ctx.available_rect();
let is_within_central_area = central_area.contains_rect(self.state.rect().shrink(1.0));
if is_within_central_area {
central_area // let's try to not cover side panels
} else {
screen_rect
}
};
let max_rect = Rect::from_min_max(
self.state.left_top_pos(),
bounds
.max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
);
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max)
.expand(clip_rect_margin)
.intersect(bounds);
let mut ui = Ui::new(
ctx.clone(),
self.layer_id,
self.layer_id.id,
max_rect,
clip_rect,
);
ui.set_enabled(self.enabled);
ui.set_visible(!self.temporarily_invisible);
ui
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Prepared {
layer_id,
mut state,
move_response,
enabled: _,
drag_bounds: _,
temporarily_invisible: _,
} = self;
state.size = content_ui.min_rect().size();
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
move_response
}
}
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
}
}
fn automatic_area_position(ctx: &Context) -> Pos2 {
let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas
.visible_windows()
.into_iter()
.map(State::rect)
.collect()
});
existing.sort_by_key(|r| r.left().round() as i32);
let available_rect = ctx.available_rect();
let spacing = 16.0;
let left = available_rect.left() + spacing;
let top = available_rect.top() + spacing;
if existing.is_empty() {
return pos2(left, top);
}
// Separate existing rectangles into columns:
let mut column_bbs = vec![existing[0]];
for &rect in &existing {
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
// same column
*current_column_bb = current_column_bb.union(rect);
} else {
// new column
column_bbs.push(rect);
}
}
{
// Look for large spaces between columns (empty columns):
let mut x = left;
for col_bb in &column_bbs {
let available = col_bb.left() - x;
if available >= 300.0 {
return pos2(x, top);
}
x = col_bb.right() + spacing;
}
}
// Find first column with some available space at the bottom of it:
for col_bb in &column_bbs {
if col_bb.bottom() < available_rect.center().y {
return pos2(col_bb.left(), col_bb.bottom() + spacing);
}
}
// Maybe we can fit a new column?
let rightmost = column_bbs.last().unwrap().right();
if rightmost + 200.0 < available_rect.right() {
return pos2(rightmost + spacing, top);
}
// Ok, just put us in the column with the most space at the bottom:
let mut best_pos = pos2(left, column_bbs[0].bottom() + spacing);
for col_bb in &column_bbs {
let col_pos = pos2(col_bb.left(), col_bb.bottom() + spacing);
if col_pos.y < best_pos.y {
best_pos = col_pos;
}
}
best_pos
}

View file

@ -1,681 +0,0 @@
use std::hash::Hash;
use crate::*;
use epaint::Shape;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct InnerState {
open: bool,
/// Height of the region when open. Used for animations
#[cfg_attr(feature = "serde", serde(default))]
open_height: Option<f32>,
}
/// This is a a building block for building collapsing regions.
///
/// It is used by [`CollapsingHeader`] and [`Window`], but can also be used on its own.
///
/// See [`CollapsingState::show_header`] for how to show a collapsing header with a custom header.
#[derive(Clone, Debug)]
pub struct CollapsingState {
id: Id,
state: InnerState,
}
impl CollapsingState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| {
d.get_persisted::<InnerState>(id)
.map(|state| Self { id, state })
})
}
pub fn store(&self, ctx: &Context) {
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
}
pub fn id(&self) -> Id {
self.id
}
pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self {
Self::load(ctx, id).unwrap_or(CollapsingState {
id,
state: InnerState {
open: default_open,
open_height: None,
},
})
}
pub fn is_open(&self) -> bool {
self.state.open
}
pub fn set_open(&mut self, open: bool) {
self.state.open = open;
}
pub fn toggle(&mut self, ui: &Ui) {
self.state.open = !self.state.open;
ui.ctx().request_repaint();
}
/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory(|mem| mem.everything_is_visible()) {
1.0
} else {
ctx.animate_bool(self.id, self.state.open)
}
}
/// Will toggle when clicked, etc.
pub(crate) fn show_default_button_with_size(
&mut self,
ui: &mut Ui,
button_size: Vec2,
) -> Response {
let (_id, rect) = ui.allocate_space(button_size);
let response = ui.interact(rect, self.id, Sense::click());
if response.clicked() {
self.toggle(ui);
}
let openness = self.openness(ui.ctx());
paint_default_icon(ui, openness, &response);
response
}
/// Will toggle when clicked, etc.
fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
self.show_button_indented(ui, paint_default_icon)
}
/// Will toggle when clicked, etc.
fn show_button_indented(
&mut self,
ui: &mut Ui,
icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
) -> Response {
let size = vec2(ui.spacing().indent, ui.spacing().icon_width);
let (_id, rect) = ui.allocate_space(size);
let response = ui.interact(rect, self.id, Sense::click());
if response.clicked() {
self.toggle(ui);
}
let (mut icon_rect, _) = ui.spacing().icon_rectangles(response.rect);
icon_rect.set_center(pos2(
response.rect.left() + ui.spacing().indent / 2.0,
response.rect.center().y,
));
let openness = self.openness(ui.ctx());
let small_icon_response = response.clone().with_new_rect(icon_rect);
icon_fn(ui, openness, &small_icon_response);
response
}
/// Shows header and body (if expanded).
///
/// The header will start with the default button in a horizontal layout, followed by whatever you add.
///
/// Will also store the state.
///
/// Returns the response of the collapsing button, the custom header, and the custom body.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let id = ui.make_persistent_id("my_collapsing_header");
/// egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, false)
/// .show_header(ui, |ui| {
/// ui.label("Header"); // you can put checkboxes or whatever here
/// })
/// .body(|ui| ui.label("Body"));
/// # });
/// ```
pub fn show_header<HeaderRet>(
mut self,
ui: &mut Ui,
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
) -> HeaderResponse<'_, HeaderRet> {
let header_response = ui.horizontal(|ui| {
let prev_item_spacing = ui.spacing_mut().item_spacing;
ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
let collapser = self.show_default_button_indented(ui);
ui.spacing_mut().item_spacing = prev_item_spacing;
(collapser, add_header(ui))
});
HeaderResponse {
state: self,
ui,
toggle_button_response: header_response.inner.0,
header_response: InnerResponse {
response: header_response.response,
inner: header_response.inner.1,
},
}
}
/// Show body if we are open, with a nice animation between closed and open.
/// Indent the body to show it belongs to the header.
///
/// Will also store the state.
pub fn show_body_indented<R>(
&mut self,
header_response: &Response,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let id = self.id;
self.show_body_unindented(ui, |ui| {
ui.indent(id, |ui| {
// make as wide as the header:
ui.expand_to_include_x(header_response.rect.right());
add_body(ui)
})
.inner
})
}
/// Show body if we are open, with a nice animation between closed and open.
/// Will also store the state.
pub fn show_body_unindented<R>(
&mut self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let openness = self.openness(ui.ctx());
if openness <= 0.0 {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None
} else if openness < 1.0 {
Some(ui.scope(|child_ui| {
let max_height = if self.state.open && self.state.open_height.is_none() {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placeholder value that shows some movement:
10.0
} else {
let full_height = self.state.open_height.unwrap_or_default();
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height)
};
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
child_ui.set_clip_rect(clip_rect);
let ret = add_body(child_ui);
let mut min_rect = child_ui.min_rect();
self.state.open_height = Some(min_rect.height());
self.store(child_ui.ctx()); // remember the height
// Pretend children took up at most `max_height` space:
min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
child_ui.force_set_min_rect(min_rect);
ret
}))
} else {
let ret_response = ui.scope(add_body);
let full_size = ret_response.response.rect.size();
self.state.open_height = Some(full_size.y);
self.store(ui.ctx()); // remember the height
Some(ret_response)
}
}
/// Paint this [CollapsingState](CollapsingState)'s toggle button. Takes an [IconPainter](IconPainter) as the icon.
/// ```
/// # egui::__run_test_ui(|ui| {
/// fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
/// let stroke = ui.style().interact(&response).fg_stroke;
/// let radius = egui::lerp(2.0..=3.0, openness);
/// ui.painter().circle_filled(response.rect.center(), radius, stroke.color);
/// }
///
/// let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
/// ui.ctx(),
/// ui.make_persistent_id("my_collapsing_state"),
/// false,
/// );
///
/// let header_res = ui.horizontal(|ui| {
/// ui.label("Header");
/// state.show_toggle_button(ui, circle_icon);
/// });
///
/// state.show_body_indented(&header_res.response, ui, |ui| ui.label("Body"));
/// # });
/// ```
pub fn show_toggle_button(
&mut self,
ui: &mut Ui,
icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static,
) -> Response {
self.show_button_indented(ui, icon_fn)
}
}
/// From [`CollapsingState::show_header`].
#[must_use = "Remember to show the body"]
pub struct HeaderResponse<'ui, HeaderRet> {
state: CollapsingState,
ui: &'ui mut Ui,
toggle_button_response: Response,
header_response: InnerResponse<HeaderRet>,
}
impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
/// Returns the response of the collapsing button, the custom header, and the custom body.
pub fn body<BodyRet>(
mut self,
add_body: impl FnOnce(&mut Ui) -> BodyRet,
) -> (
Response,
InnerResponse<HeaderRet>,
Option<InnerResponse<BodyRet>>,
) {
let body_response =
self.state
.show_body_indented(&self.header_response.response, self.ui, add_body);
(
self.toggle_button_response,
self.header_response,
body_response,
)
}
/// Returns the response of the collapsing button, the custom header, and the custom body, without indentation.
pub fn body_unindented<BodyRet>(
mut self,
add_body: impl FnOnce(&mut Ui) -> BodyRet,
) -> (
Response,
InnerResponse<HeaderRet>,
Option<InnerResponse<BodyRet>>,
) {
let body_response = self.state.show_body_unindented(self.ui, add_body);
(
self.toggle_button_response,
self.header_response,
body_response,
)
}
}
// ----------------------------------------------------------------------------
/// Paint the arrow icon that indicated if the region is open or not
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
let visuals = ui.style().interact(response);
let rect = response.rect;
// Draw a pointy triangle arrow:
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
let rect = rect.expand(visuals.expansion);
let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
use std::f32::consts::TAU;
let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
*p = rect.center() + rotation * (*p - rect.center());
}
ui.painter().add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
}
/// A function that paints an icon indicating if the region is open or not
pub type IconPainter = Box<dyn FnOnce(&mut Ui, f32, &Response)>;
/// A header which can be collapsed/expanded, revealing a contained [`Ui`] region.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::CollapsingHeader::new("Heading")
/// .show(ui, |ui| {
/// ui.label("Body");
/// });
///
/// // Short version:
/// ui.collapsing("Heading", |ui| { ui.label("Body"); });
/// # });
/// ```
///
/// If you want to customize the header contents, see [`CollapsingState::show_header`].
#[must_use = "You should call .show()"]
pub struct CollapsingHeader {
text: WidgetText,
default_open: bool,
open: Option<bool>,
id_source: Id,
enabled: bool,
selectable: bool,
selected: bool,
show_background: bool,
icon: Option<IconPainter>,
}
impl CollapsingHeader {
/// The [`CollapsingHeader`] starts out collapsed unless you call `default_open`.
///
/// The label is used as an [`Id`] source.
/// If the label is unique and static this is fine,
/// but if it changes or there are several [`CollapsingHeader`] with the same title
/// you need to provide a unique id source with [`Self::id_source`].
pub fn new(text: impl Into<WidgetText>) -> Self {
let text = text.into();
let id_source = Id::new(text.text());
Self {
text,
default_open: false,
open: None,
id_source,
enabled: true,
selectable: false,
selected: false,
show_background: false,
icon: None,
}
}
/// By default, the [`CollapsingHeader`] is collapsed.
/// Call `.default_open(true)` to change this.
pub fn default_open(mut self, open: bool) -> Self {
self.default_open = open;
self
}
/// Calling `.open(Some(true))` will make the collapsing header open this frame (or stay open).
///
/// Calling `.open(Some(false))` will make the collapsing header close this frame (or stay closed).
///
/// Calling `.open(None)` has no effect (default).
pub fn open(mut self, open: Option<bool>) -> Self {
self.open = open;
self
}
/// Explicitly set the source of the [`Id`] of this widget, instead of using title label.
/// This is useful if the title label is dynamic or not unique.
pub fn id_source(mut self, id_source: impl Hash) -> Self {
self.id_source = Id::new(id_source);
self
}
/// If you set this to `false`, the [`CollapsingHeader`] will be grayed out and un-clickable.
///
/// This is a convenience for [`Ui::set_enabled`].
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Can the [`CollapsingHeader`] be selected by clicking it? Default: `false`.
#[deprecated = "Use the more powerful egui::collapsing_header::CollapsingState::show_header"] // Deprecated in 2022-04-28, before egui 0.18
pub fn selectable(mut self, selectable: bool) -> Self {
self.selectable = selectable;
self
}
/// If you set this to 'true', the [`CollapsingHeader`] will be shown as selected.
///
/// Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// let mut selected = false;
/// let response = egui::CollapsingHeader::new("Select and open me")
/// .selectable(true)
/// .selected(selected)
/// .show(ui, |ui| ui.label("Body"));
/// if response.header_response.clicked() {
/// selected = true;
/// }
/// # });
/// ```
#[deprecated = "Use the more powerful egui::collapsing_header::CollapsingState::show_header"] // Deprecated in 2022-04-28, before egui 0.18
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
/// Should the [`CollapsingHeader`] show a background behind it? Default: `false`.
///
/// To show it behind all [`CollapsingHeader`] you can just use:
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.visuals_mut().collapsing_header_frame = true;
/// # });
/// ```
pub fn show_background(mut self, show_background: bool) -> Self {
self.show_background = show_background;
self
}
/// Use the provided function to render a different [`CollapsingHeader`] icon.
/// Defaults to a triangle that animates as the [`CollapsingHeader`] opens and closes.
///
/// For example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
/// let stroke = ui.style().interact(&response).fg_stroke;
/// let radius = egui::lerp(2.0..=3.0, openness);
/// ui.painter().circle_filled(response.rect.center(), radius, stroke.color);
/// }
///
/// egui::CollapsingHeader::new("Circles")
/// .icon(circle_icon)
/// .show(ui, |ui| { ui.label("Hi!"); });
/// # });
/// ```
pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self {
self.icon = Some(Box::new(icon_fn));
self
}
}
struct Prepared {
header_response: Response,
state: CollapsingState,
openness: f32,
}
impl CollapsingHeader {
fn begin(self, ui: &mut Ui) -> Prepared {
assert!(
ui.layout().main_dir().is_vertical(),
"Horizontal collapsing is unimplemented"
);
let Self {
icon,
text,
default_open,
open,
id_source,
enabled: _,
selectable,
selected,
show_background,
} = self;
// TODO(emilk): horizontal layout, with icon and text as labels. Insert background behind using Frame.
let id = ui.make_persistent_id(id_source);
let button_padding = ui.spacing().button_padding;
let available = ui.available_rect_before_wrap();
let text_pos = available.min + vec2(ui.spacing().indent, 0.0);
let wrap_width = available.right() - text_pos.x;
let wrap = Some(false);
let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button);
let text_max_x = text_pos.x + text.size().x;
let mut desired_width = text_max_x + button_padding.x - available.left();
if ui.visuals().collapsing_header_frame {
desired_width = desired_width.max(available.width()); // fill full width
}
let mut desired_size = vec2(desired_width, text.size().y + 2.0 * button_padding.y);
desired_size = desired_size.at_least(ui.spacing().interact_size);
let (_, rect) = ui.allocate_space(desired_size);
let mut header_response = ui.interact(rect, id, Sense::click());
let text_pos = pos2(
text_pos.x,
header_response.rect.center().y - text.size().y / 2.0,
);
let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open);
if let Some(open) = open {
if open != state.is_open() {
state.toggle(ui);
header_response.mark_changed();
}
} else if header_response.clicked() {
state.toggle(ui);
header_response.mark_changed();
}
header_response
.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text()));
let openness = state.openness(ui.ctx());
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact_selectable(&header_response, selected);
if ui.visuals().collapsing_header_frame || show_background {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
});
}
if selected || selectable && (header_response.hovered() || header_response.has_focus())
{
let rect = rect.expand(visuals.expansion);
ui.painter()
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
}
{
let (mut icon_rect, _) = ui.spacing().icon_rectangles(header_response.rect);
icon_rect.set_center(pos2(
header_response.rect.left() + ui.spacing().indent / 2.0,
header_response.rect.center().y,
));
let icon_response = header_response.clone().with_new_rect(icon_rect);
if let Some(icon) = icon {
icon(ui, openness, &icon_response);
} else {
paint_default_icon(ui, openness, &icon_response);
}
}
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
}
Prepared {
header_response,
state,
openness,
}
}
#[inline]
pub fn show<R>(
self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_body), true)
}
#[inline]
pub fn show_unindented<R>(
self,
ui: &mut Ui,
add_body: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
self.show_dyn(ui, Box::new(add_body), false)
}
fn show_dyn<'c, R>(
self,
ui: &mut Ui,
add_body: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
indented: bool,
) -> CollapsingResponse<R> {
// Make sure body is bellow header,
// and make sure it is one unit (necessary for putting a [`CollapsingHeader`] in a grid).
ui.vertical(|ui| {
ui.set_enabled(self.enabled);
let Prepared {
header_response,
mut state,
openness,
} = self.begin(ui); // show the header
let ret_response = if indented {
state.show_body_indented(&header_response, ui, add_body)
} else {
state.show_body_unindented(ui, add_body)
};
if let Some(ret_response) = ret_response {
CollapsingResponse {
header_response,
body_response: Some(ret_response.response),
body_returned: Some(ret_response.inner),
openness,
}
} else {
CollapsingResponse {
header_response,
body_response: None,
body_returned: None,
openness,
}
}
})
.inner
}
}
/// The response from showing a [`CollapsingHeader`].
pub struct CollapsingResponse<R> {
/// Response of the actual clickable header.
pub header_response: Response,
/// None iff collapsed.
pub body_response: Option<Response>,
/// None iff collapsed.
pub body_returned: Option<R>,
/// 0.0 if fully closed, 1.0 if fully open, and something in-between while animating.
pub openness: f32,
}
impl<R> CollapsingResponse<R> {
/// Was the [`CollapsingHeader`] fully closed (and not being animated)?
pub fn fully_closed(&self) -> bool {
self.openness <= 0.0
}
/// Was the [`CollapsingHeader`] fully open (and not being animated)?
pub fn fully_open(&self) -> bool {
self.openness >= 1.0
}
}

View file

@ -1,429 +0,0 @@
use epaint::Shape;
use crate::{style::WidgetVisuals, *};
/// Indicate wether or not a popup will be shown above or below the box.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
/// A function that paints the [`ComboBox`] icon
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
/// A drop-down selection menu with a descriptive label.
///
/// ```
/// # #[derive(Debug, PartialEq)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// # egui::__run_test_ui(|ui| {
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
/// ui.selectable_value(&mut selected, Enum::First, "First");
/// ui.selectable_value(&mut selected, Enum::Second, "Second");
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
/// # });
/// ```
#[must_use = "You should call .show*"]
pub struct ComboBox {
id_source: Id,
label: Option<WidgetText>,
selected_text: WidgetText,
width: Option<f32>,
icon: Option<IconPainter>,
wrap_enabled: bool,
}
impl ComboBox {
/// Create new [`ComboBox`] with id and label
pub fn new(id_source: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
Self {
id_source: Id::new(id_source),
label: Some(label.into()),
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
/// Label shown next to the combo box
pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into();
Self {
id_source: Id::new(label.text()),
label: Some(label),
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
/// Without label.
pub fn from_id_source(id_source: impl std::hash::Hash) -> Self {
Self {
id_source: Id::new(id_source),
label: Default::default(),
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
/// Set the outer width of the button and menu.
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
/// What we show as the currently selected value
pub fn selected_text(mut self, selected_text: impl Into<WidgetText>) -> Self {
self.selected_text = selected_text.into();
self
}
/// Use the provided function to render a different [`ComboBox`] icon.
/// Defaults to a triangle that expands when the cursor is hovering over the [`ComboBox`].
///
/// For example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let text = "Selected text";
/// pub fn filled_triangle(
/// ui: &egui::Ui,
/// rect: egui::Rect,
/// visuals: &egui::style::WidgetVisuals,
/// _is_open: bool,
/// _above_or_below: egui::AboveOrBelow,
/// ) {
/// let rect = egui::Rect::from_center_size(
/// rect.center(),
/// egui::vec2(rect.width() * 0.6, rect.height() * 0.4),
/// );
/// ui.painter().add(egui::Shape::convex_polygon(
/// vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
/// visuals.fg_stroke.color,
/// visuals.fg_stroke,
/// ));
/// }
///
/// egui::ComboBox::from_id_source("my-combobox")
/// .selected_text(text)
/// .icon(filled_triangle)
/// .show_ui(ui, |_ui| {});
/// # });
/// ```
pub fn icon(
mut self,
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
) -> Self {
self.icon = Some(Box::new(icon_fn));
self
}
/// Controls whether text wrap is used for the selected text
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap_enabled = wrap;
self
}
/// Show the combo box, with the given ui code for the menu contents.
///
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
pub fn show_ui<R>(
self,
ui: &mut Ui,
menu_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
self.show_ui_dyn(ui, Box::new(menu_contents))
}
fn show_ui_dyn<'c, R>(
self,
ui: &mut Ui,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let Self {
id_source,
label,
selected_text,
width,
icon,
wrap_enabled,
} = self;
let button_id = ui.make_persistent_id(id_source);
ui.horizontal(|ui| {
let mut ir = combo_box_dyn(
ui,
button_id,
selected_text,
menu_contents,
icon,
wrap_enabled,
width,
);
if let Some(label) = label {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ""));
}
ir
})
.inner
}
/// Show a list of items with the given selected index.
///
///
/// ```
/// # #[derive(Debug, PartialEq)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// # egui::__run_test_ui(|ui| {
/// let alternatives = ["a", "b", "c", "d"];
/// let mut selected = 2;
/// egui::ComboBox::from_label("Select one!").show_index(
/// ui,
/// &mut selected,
/// alternatives.len(),
/// |i| alternatives[i].to_owned()
/// );
/// # });
/// ```
pub fn show_index(
self,
ui: &mut Ui,
selected: &mut usize,
len: usize,
get: impl Fn(usize) -> String,
) -> Response {
let slf = self.selected_text(get(*selected));
let mut changed = false;
let mut response = slf
.show_ui(ui, |ui| {
for i in 0..len {
if ui.selectable_label(i == *selected, get(i)).clicked() {
*selected = i;
changed = true;
}
}
})
.response;
if changed {
response.mark_changed();
}
response
}
}
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,
selected_text: WidgetText,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>,
wrap_enabled: bool,
width: Option<f32>,
) -> InnerResponse<Option<R>> {
let popup_id = button_id.with("popup");
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
< ui.ctx().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
AboveOrBelow::Above
};
let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
let icon_spacing = ui.spacing().icon_spacing;
// We don't want to change width when user selects something new
let full_minimum_width = if wrap_enabled {
// Currently selected value's text will be wrapped if needed, so occupy the available width.
ui.available_width()
} else {
// Occupy at least the minimum width assigned to ComboBox.
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
width - 2.0 * margin.x
};
let icon_size = Vec2::splat(ui.spacing().icon_width);
let wrap_width = if wrap_enabled {
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
ui.available_width() - icon_spacing - icon_size.x
} else {
// Use all the width necessary to display the currently selected value's text.
f32::INFINITY
};
let galley =
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
// The width necessary to contain the whole widget with the currently selected value's text.
let width = if wrap_enabled {
full_minimum_width
} else {
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
galley.size().x + icon_spacing + icon_size.x
};
// Case : wrap_enabled : occupy all the available width.
// Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox,
// increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)).
let width = width.at_least(full_minimum_width);
let height = galley.size().y.max(icon_size.y);
let (_, rect) = ui.allocate_space(Vec2::new(width, height));
let button_rect = ui.min_rect().expand2(ui.spacing().button_padding);
let response = ui.interact(button_rect, button_id, Sense::click());
// response.active |= is_popup_open;
if ui.is_rect_visible(rect) {
let icon_rect = Align2::RIGHT_CENTER.align_size_within_rect(icon_size, rect);
let visuals = if is_popup_open {
&ui.visuals().widgets.open
} else {
ui.style().interact(&response)
};
if let Some(icon) = icon {
icon(
ui,
icon_rect.expand(visuals.expansion),
visuals,
is_popup_open,
above_or_below,
);
} else {
paint_default_icon(
ui.painter(),
icon_rect.expand(visuals.expansion),
visuals,
above_or_below,
);
}
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
galley.paint_with_visuals(ui.painter(), text_rect.min, visuals);
}
});
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
let inner = crate::popup::popup_above_or_below_widget(
ui,
popup_id,
&button_response,
above_or_below,
|ui| {
ScrollArea::vertical()
.max_height(ui.spacing().combo_height)
.show(ui, menu_contents)
.inner
},
);
InnerResponse {
inner,
response: button_response,
}
}
fn button_frame(
ui: &mut Ui,
id: Id,
is_popup_open: bool,
sense: Sense,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let where_to_put_background = ui.painter().add(Shape::Noop);
let margin = ui.spacing().button_padding;
let interact_size = ui.spacing().interact_size;
let mut outer_rect = ui.available_rect_before_wrap();
outer_rect.set_height(outer_rect.height().at_least(interact_size.y));
let inner_rect = outer_rect.shrink2(margin);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
add_contents(&mut content_ui);
let mut outer_rect = content_ui.min_rect().expand2(margin);
outer_rect.set_height(outer_rect.height().at_least(interact_size.y));
let response = ui.interact(outer_rect, id, sense);
if ui.is_rect_visible(outer_rect) {
let visuals = if is_popup_open {
&ui.visuals().widgets.open
} else {
ui.style().interact(&response)
};
ui.painter().set(
where_to_put_background,
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
stroke: visuals.bg_stroke,
},
);
}
ui.advance_cursor_after_rect(outer_rect);
response
}
fn paint_default_icon(
painter: &Painter,
rect: Rect,
visuals: &WidgetVisuals,
above_or_below: AboveOrBelow,
) {
let rect = Rect::from_center_size(
rect.center(),
vec2(rect.width() * 0.7, rect.height() * 0.45),
);
match above_or_below {
AboveOrBelow::Above => {
// Upward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
AboveOrBelow::Below => {
// Downward pointing triangle
painter.add(Shape::convex_polygon(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
}
}

View file

@ -1,288 +0,0 @@
//! Frame container
use crate::{layers::ShapeIdx, style::Margin, *};
use epaint::*;
/// Add a background, frame and/or margin to a rectangular background of a [`Ui`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::Frame::none()
/// .fill(egui::Color32::RED)
/// .show(ui, |ui| {
/// ui.label("Label with red background");
/// });
/// # });
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[must_use = "You should call .show()"]
pub struct Frame {
/// Margin within the painted frame.
pub inner_margin: Margin,
/// Margin outside the painted frame.
pub outer_margin: Margin,
pub rounding: Rounding,
pub shadow: Shadow,
pub fill: Color32,
pub stroke: Stroke,
}
impl Frame {
pub fn none() -> Self {
Self::default()
}
/// For when you want to group a few widgets together within a frame.
pub fn group(style: &Style) -> Self {
Self {
inner_margin: Margin::same(6.0), // same and symmetric looks best in corners when nesting groups
rounding: style.visuals.widgets.noninteractive.rounding,
stroke: style.visuals.widgets.noninteractive.bg_stroke,
..Default::default()
}
}
pub fn side_top_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::symmetric(8.0, 2.0),
fill: style.visuals.panel_fill,
..Default::default()
}
}
pub fn central_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::same(8.0),
fill: style.visuals.panel_fill,
..Default::default()
}
}
pub fn window(style: &Style) -> Self {
Self {
inner_margin: style.spacing.window_margin,
rounding: style.visuals.window_rounding,
shadow: style.visuals.window_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}
pub fn menu(style: &Style) -> Self {
Self {
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}
pub fn popup(style: &Style) -> Self {
Self {
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.menu_rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}
/// A canvas to draw on.
///
/// In bright mode this will be very bright,
/// and in dark mode this will be very dark.
pub fn canvas(style: &Style) -> Self {
Self {
inner_margin: Margin::same(2.0),
rounding: style.visuals.widgets.noninteractive.rounding,
fill: style.visuals.extreme_bg_color,
stroke: style.visuals.window_stroke(),
..Default::default()
}
}
/// A dark canvas to draw on.
pub fn dark_canvas(style: &Style) -> Self {
Self {
fill: Color32::from_black_alpha(250),
..Self::canvas(style)
}
}
}
impl Frame {
#[inline]
pub fn fill(mut self, fill: Color32) -> Self {
self.fill = fill;
self
}
#[inline]
pub fn stroke(mut self, stroke: Stroke) -> Self {
self.stroke = stroke;
self
}
#[inline]
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into();
self
}
/// Margin within the painted frame.
#[inline]
pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
self.inner_margin = inner_margin.into();
self
}
/// Margin outside the painted frame.
#[inline]
pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
self.outer_margin = outer_margin.into();
self
}
#[deprecated = "Renamed inner_margin in egui 0.18"]
#[inline]
pub fn margin(self, margin: impl Into<Margin>) -> Self {
self.inner_margin(margin)
}
#[inline]
pub fn shadow(mut self, shadow: Shadow) -> Self {
self.shadow = shadow;
self
}
pub fn multiply_with_opacity(mut self, opacity: f32) -> Self {
self.fill = self.fill.linear_multiply(opacity);
self.stroke.color = self.stroke.color.linear_multiply(opacity);
self.shadow.color = self.shadow.color.linear_multiply(opacity);
self
}
}
impl Frame {
/// inner margin plus outer margin.
#[inline]
pub fn total_margin(&self) -> Margin {
self.inner_margin + self.outer_margin
}
}
// ----------------------------------------------------------------------------
pub struct Prepared {
pub frame: Frame,
where_to_put_background: ShapeIdx,
pub content_ui: Ui,
}
impl Frame {
pub fn begin(self, ui: &mut Ui) -> Prepared {
let where_to_put_background = ui.painter().add(Shape::Noop);
let outer_rect_bounds = ui.available_rect_before_wrap();
let mut inner_rect = outer_rect_bounds;
inner_rect.min += self.outer_margin.left_top() + self.inner_margin.left_top();
inner_rect.max -= self.outer_margin.right_bottom() + self.inner_margin.right_bottom();
// Make sure we don't shrink to the negative:
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y);
let content_ui = ui.child_ui(inner_rect, *ui.layout());
// content_ui.set_clip_rect(outer_rect_bounds.shrink(self.stroke.width * 0.5)); // Can't do this since we don't know final size yet
Prepared {
frame: self,
where_to_put_background,
content_ui,
}
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.show_dyn(ui, Box::new(add_contents))
}
fn show_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let mut prepared = self.begin(ui);
let ret = add_contents(&mut prepared.content_ui);
let response = prepared.end(ui);
InnerResponse::new(ret, response)
}
pub fn paint(&self, outer_rect: Rect) -> Shape {
let Self {
inner_margin: _,
outer_margin: _,
rounding,
shadow,
fill,
stroke,
} = *self;
let frame_shape = Shape::Rect(epaint::RectShape {
rect: outer_rect,
rounding,
fill,
stroke,
});
if shadow == Default::default() {
frame_shape
} else {
let shadow = shadow.tessellate(outer_rect, rounding);
let shadow = Shape::Mesh(shadow);
Shape::Vec(vec![shadow, frame_shape])
}
}
}
impl Prepared {
fn paint_rect(&self) -> Rect {
let mut rect = self.content_ui.min_rect();
rect.min -= self.frame.inner_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom();
rect
}
fn content_with_margin(&self) -> Rect {
let mut rect = self.content_ui.min_rect();
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
rect
}
pub fn end(self, ui: &mut Ui) -> Response {
let paint_rect = self.paint_rect();
let Prepared {
frame,
where_to_put_background,
..
} = self;
if ui.is_rect_visible(paint_rect) {
let shape = frame.paint(paint_rect);
ui.painter().set(where_to_put_background, shape);
}
ui.allocate_rect(self.content_with_margin(), Sense::hover())
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,378 +0,0 @@
//! Show popup windows, tooltips, context menus etc.
use crate::*;
// ----------------------------------------------------------------------------
/// Same state for all tooltips.
#[derive(Clone, Debug, Default)]
pub(crate) struct TooltipState {
last_common_id: Option<Id>,
individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
}
impl TooltipState {
pub fn load(ctx: &Context) -> Option<Self> {
ctx.data_mut(|d| d.get_temp(Id::null()))
}
fn store(self, ctx: &Context) {
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
}
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
if self.last_common_id == Some(common_id) {
Some(self.individual_ids_and_sizes.get(&index)?.1)
} else {
None
}
}
fn set_individual_tooltip(
&mut self,
common_id: Id,
index: usize,
individual_id: Id,
size: Vec2,
) {
if self.last_common_id != Some(common_id) {
self.last_common_id = Some(common_id);
self.individual_ids_and_sizes.clear();
}
self.individual_ids_and_sizes
.insert(index, (individual_id, size));
}
}
// ----------------------------------------------------------------------------
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
/// });
/// }
/// # });
/// ```
pub fn show_tooltip<R>(
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
show_tooltip_at_pointer(ctx, id, add_contents)
}
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_at_pointer(ui.ctx(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
/// });
/// }
/// # });
/// ```
pub fn show_tooltip_at_pointer<R>(
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let suggested_pos = ctx
.input(|i| i.pointer.hover_pos())
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
show_tooltip_at(ctx, id, suggested_pos, add_contents)
}
/// Show a tooltip under the given area.
///
/// If the tooltip does not fit under the area, it tries to place it above it instead.
pub fn show_tooltip_for<R>(
ctx: &Context,
id: Id,
rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
let (above, position) = if ctx.input(|i| i.any_touches()) {
(true, expanded_rect.left_top())
} else {
(false, expanded_rect.left_bottom())
};
show_tooltip_at_avoid_dyn(
ctx,
id,
Some(position),
above,
expanded_rect,
Box::new(add_contents),
)
}
/// Show a tooltip at the given position.
///
/// Returns `None` if the tooltip could not be placed.
pub fn show_tooltip_at<R>(
ctx: &Context,
id: Id,
suggested_position: Option<Pos2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let above = false;
show_tooltip_at_avoid_dyn(
ctx,
id,
suggested_position,
above,
Rect::NOTHING,
Box::new(add_contents),
)
}
fn show_tooltip_at_avoid_dyn<'c, R>(
ctx: &Context,
individual_id: Id,
suggested_position: Option<Pos2>,
above: bool,
mut avoid_rect: Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<R> {
let spacing = 4.0;
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
let mut frame_state =
ctx.frame_state(|fs| fs.tooltip_state)
.unwrap_or(crate::frame_state::TooltipFrameState {
common_id: individual_id,
rect: Rect::NOTHING,
count: 0,
});
let mut position = if frame_state.rect.is_positive() {
avoid_rect = avoid_rect.union(frame_state.rect);
if above {
frame_state.rect.left_top() - spacing * Vec2::Y
} else {
frame_state.rect.left_bottom() + spacing * Vec2::Y
}
} else if let Some(position) = suggested_position {
position
} else if ctx.memory(|mem| mem.everything_is_visible()) {
Pos2::ZERO
} else {
return None; // No good place for a tooltip :(
};
let mut long_state = TooltipState::load(ctx).unwrap_or_default();
let expected_size =
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
if above {
position.y -= expected_size.y;
}
position = position.at_most(ctx.screen_rect().max - expected_size);
// check if we intersect the avoid_rect
{
let new_rect = Rect::from_min_size(position, expected_size);
// Note: We use shrink so that we don't get false positives when the rects just touch
if new_rect.shrink(1.0).intersects(avoid_rect) {
if above {
// place below instead:
position = avoid_rect.left_bottom() + spacing * Vec2::Y;
} else {
// place above instead:
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
}
}
}
let position = position.at_least(ctx.screen_rect().min);
let area_id = frame_state.common_id.with(frame_state.count);
let InnerResponse { inner, response } =
show_tooltip_area_dyn(ctx, area_id, position, add_contents);
long_state.set_individual_tooltip(
frame_state.common_id,
frame_state.count,
individual_id,
response.rect.size(),
);
long_state.store(ctx);
frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
Some(inner)
}
/// Show some text at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_text`].
///
/// See also [`show_tooltip`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text");
/// }
/// # });
/// ```
pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) -> Option<()> {
show_tooltip(ctx, id, |ui| {
crate::widgets::Label::new(text).ui(ui);
})
}
/// Show a pop-over window.
fn show_tooltip_area_dyn<'c, R>(
ctx: &Context,
area_id: Id,
window_pos: Pos2,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
use containers::*;
Area::new(area_id)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.constrain(true)
.interactable(false)
.drag_bounds(ctx.screen_rect())
.show(ctx, |ui| {
Frame::popup(&ctx.style())
.show(ui, |ui| {
ui.set_max_width(ui.spacing().tooltip_width);
add_contents(ui)
})
.inner
})
}
/// Was this popup visible last frame?
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if let Some(state) = TooltipState::load(ctx) {
if let Some(common_id) = state.last_common_id {
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
if *individual_id == tooltip_id {
let area_id = common_id.with(count);
let layer_id = LayerId::new(Order::Tooltip, area_id);
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
return true;
}
}
}
}
}
false
}
/// Helper for [`popup_above_or_below_widget`].
pub fn popup_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
popup_above_or_below_widget(
ui,
popup_id,
widget_response,
AboveOrBelow::Below,
add_contents,
)
}
/// Shows a popup above or below another widget.
///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
///
/// The opened popup will have the same width as the parent.
///
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
///
/// Returns `None` if the popup is not open.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() {
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// }
/// let below = egui::AboveOrBelow::Below;
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
/// ui.set_min_width(200.0); // if you want to control the size
/// ui.label("Some more info, or things you can select:");
/// ui.label("…");
/// });
/// # });
/// ```
pub fn popup_above_or_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
above_or_below: AboveOrBelow,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let (pos, pivot) = match above_or_below {
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
};
let inner = Area::new(popup_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.pivot(pivot)
.show(ui.ctx(), |ui| {
// Note: we use a separate clip-rect for this area, so the popup can be outside the parent.
// See https://github.com/emilk/egui/issues/825
let frame = Frame::popup(ui.style());
let frame_margin = frame.total_margin();
frame
.show(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
ui.set_width(widget_response.rect.width() - frame_margin.sum().x);
add_contents(ui)
})
.inner
})
.inner
})
.inner;
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
ui.memory_mut(|mem| mem.close_popup());
}
Some(inner)
} else {
None
}
}

View file

@ -1,912 +0,0 @@
//! Coordinate system names:
//! * content: size of contents (generally large; that's why we want scroll bars)
//! * outer: size of scroll area including scroll bar(s)
//! * inner: excluding scroll bar(s). The area we clip the contents to.
#![allow(clippy::needless_range_loop)]
use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct State {
/// Positive offset means scrolling down/right
pub offset: Vec2,
/// Were the scroll bars visible last frame?
show_scroll: [bool; 2],
/// The content were to large to fit large frame.
content_is_too_large: [bool; 2],
/// Momentum, used for kinetic scrolling
#[cfg_attr(feature = "serde", serde(skip))]
vel: Vec2,
/// Mouse offset relative to the top of the handle when started moving the handle.
scroll_start_offset_from_top_left: [Option<f32>; 2],
/// Is the scroll sticky. This is true while scroll handle is in the end position
/// and remains that way until the user moves the scroll_handle. Once unstuck (false)
/// it remains false until the scroll touches the end position, which reenables stickiness.
scroll_stuck_to_end: [bool; 2],
}
impl Default for State {
fn default() -> Self {
Self {
offset: Vec2::ZERO,
show_scroll: [false; 2],
content_is_too_large: [false; 2],
vel: Vec2::ZERO,
scroll_start_offset_from_top_left: [None; 2],
scroll_stuck_to_end: [true; 2],
}
}
}
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| d.get_persisted(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_persisted(id, self));
}
}
pub struct ScrollAreaOutput<R> {
/// What the user closure returned.
pub inner: R,
/// [`Id`] of the [`ScrollArea`].
pub id: Id,
/// The current state of the scroll area.
pub state: State,
/// The size of the content. If this is larger than [`Self::inner_rect`],
/// then there was need for scrolling.
pub content_size: Vec2,
/// Where on the screen the content is (excludes scroll bars).
pub inner_rect: Rect,
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::ScrollArea::vertical().show(ui, |ui| {
/// // Add a lot of widgets here.
/// });
/// # });
/// ```
///
/// You can scroll to an element using [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`] and [`Ui::scroll_to_rect`].
#[derive(Clone, Debug)]
#[must_use = "You should call .show()"]
pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling?
has_bar: [bool; 2],
auto_shrink: [bool; 2],
max_size: Vec2,
min_scrolled_size: Vec2,
always_show_scroll: bool,
id_source: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
/// again once scroll handle makes contact with end.
stick_to_end: [bool; 2],
}
impl ScrollArea {
/// Create a horizontal scroll area.
pub fn horizontal() -> Self {
Self::new([true, false])
}
/// Create a vertical scroll area.
pub fn vertical() -> Self {
Self::new([false, true])
}
/// Create a bi-directional (horizontal and vertical) scroll area.
pub fn both() -> Self {
Self::new([true, true])
}
/// Create a scroll area where both direction of scrolling is disabled.
/// It's unclear why you would want to do this.
pub fn neither() -> Self {
Self::new([false, false])
}
/// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollAre::new([true, false])` enable horizontal scrolling.
pub fn new(has_bar: [bool; 2]) -> Self {
Self {
has_bar,
auto_shrink: [true; 2],
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
always_show_scroll: false,
id_source: None,
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2],
}
}
/// The maximum width of the outer frame of the scroll area.
///
/// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
///
/// See also [`Self::auto_shrink`].
pub fn max_width(mut self, max_width: f32) -> Self {
self.max_size.x = max_width;
self
}
/// The maximum height of the outer frame of the scroll area.
///
/// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default).
///
/// See also [`Self::auto_shrink`].
pub fn max_height(mut self, max_height: f32) -> Self {
self.max_size.y = max_height;
self
}
/// The minimum width of a horizontal scroll area which requires scroll bars.
///
/// The [`ScrollArea`] will only become smaller than this if the content is smaller than this
/// (and so we don't require scroll bars).
///
/// Default: `64.0`.
pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self {
self.min_scrolled_size.x = min_scrolled_width;
self
}
/// The minimum height of a vertical scroll area which requires scroll bars.
///
/// The [`ScrollArea`] will only become smaller than this if the content is smaller than this
/// (and so we don't require scroll bars).
///
/// Default: `64.0`.
pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self {
self.min_scrolled_size.y = min_scrolled_height;
self
}
/// If `false` (default), the scroll bar will be hidden when not needed/
/// If `true`, the scroll bar will always be displayed even if not needed.
pub fn always_show_scroll(mut self, always_show_scroll: bool) -> Self {
self.always_show_scroll = always_show_scroll;
self
}
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
self.id_source = Some(Id::new(id_source));
self
}
/// Set the horizontal and vertical scroll offset position.
///
/// Positive offset means scrolling down/right.
///
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn scroll_offset(mut self, offset: Vec2) -> Self {
self.offset_x = Some(offset.x);
self.offset_y = Some(offset.y);
self
}
/// Set the vertical scroll offset position.
///
/// Positive offset means scrolling down.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
self.offset_y = Some(offset);
self
}
/// Set the horizontal scroll offset position.
///
/// Positive offset means scrolling right.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
self.offset_x = Some(offset);
self
}
/// Turn on/off scrolling on the horizontal axis.
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.has_bar[0] = hscroll;
self
}
/// Turn on/off scrolling on the vertical axis.
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.has_bar[1] = vscroll;
self
}
/// Turn on/off scrolling on the horizontal/vertical axes.
pub fn scroll2(mut self, has_bar: [bool; 2]) -> Self {
self.has_bar = has_bar;
self
}
/// Control the scrolling behavior.
///
/// * If `true` (default), the scroll area will respond to user scrolling.
/// * If `false`, the scroll area will not respond to user scrolling.
///
/// This can be used, for example, to optionally freeze scrolling while the user
/// is typing text in a [`TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable;
self
}
/// Can the user drag the scroll area to scroll?
///
/// This is useful for touch screens.
///
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self
}
/// For each axis, should the containing area shrink if the content is small?
///
/// * If `true`, egui will add blank space outside the scroll area.
/// * If `false`, egui will add blank space inside the scroll area.
///
/// Default: `[true; 2]`.
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
self.auto_shrink = auto_shrink;
self
}
pub(crate) fn has_any_bar(&self) -> bool {
self.has_bar[0] || self.has_bar[1]
}
/// The scroll handle will stick to the rightmost position even while the content size
/// changes dynamically. This can be useful to simulate text scrollers coming in from right
/// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck"
/// it will remain focused on whatever content viewport the user left it on. If the scroll
/// handle is dragged all the way to the right it will again become stuck and remain there
/// until manually pulled from the end position.
pub fn stick_to_right(mut self, stick: bool) -> Self {
self.stick_to_end[0] = stick;
self
}
/// The scroll handle will stick to the bottom position even while the content size
/// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
/// The scroll handle remains stuck until user manually changes position. Once "unstuck"
/// it will remain focused on whatever content viewport the user left it on. If the scroll
/// handle is dragged to the bottom it will again become stuck and remain there until manually
/// pulled from the end position.
pub fn stick_to_bottom(mut self, stick: bool) -> Self {
self.stick_to_end[1] = stick;
self
}
}
struct Prepared {
id: Id,
state: State,
has_bar: [bool; 2],
auto_shrink: [bool; 2],
/// How much horizontal and vertical space are used up by the
/// width of the vertical bar, and the height of the horizontal bar?
current_bar_use: Vec2,
always_show_scroll: bool,
/// Where on the screen the content is (excludes scroll bars).
inner_rect: Rect,
content_ui: Ui,
/// Relative coordinates: the offset and size of the view of the inner UI.
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
stick_to_end: [bool; 2],
}
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
has_bar,
auto_shrink,
max_size,
min_scrolled_size,
always_show_scroll,
id_source,
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
stick_to_end,
} = self;
let ctx = ui.ctx().clone();
let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_source);
ui.ctx().check_for_id_clash(
id,
Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO),
"ScrollArea",
);
let mut state = State::load(&ctx, id).unwrap_or_default();
state.offset.x = offset_x.unwrap_or(state.offset.x);
state.offset.y = offset_y.unwrap_or(state.offset.y);
let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui);
let current_hscroll_bar_height = if !has_bar[0] {
0.0
} else if always_show_scroll {
max_scroll_bar_width
} else {
max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), state.show_scroll[0])
};
let current_vscroll_bar_width = if !has_bar[1] {
0.0
} else if always_show_scroll {
max_scroll_bar_width
} else {
max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), state.show_scroll[1])
};
let current_bar_use = vec2(current_vscroll_bar_width, current_hscroll_bar_height);
let available_outer = ui.available_rect_before_wrap();
let outer_size = available_outer.size().at_most(max_size);
let inner_size = {
let mut inner_size = outer_size - current_bar_use;
// Don't go so far that we shrink to zero.
// In particular, if we put a [`ScrollArea`] inside of a [`ScrollArea`], the inner
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if has_bar[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
inner_size
};
let inner_rect = Rect::from_min_size(available_outer.min, inner_size);
let mut content_max_size = inner_size;
if true {
// Tell the inner Ui to *try* to fit the content without needing to scroll,
// i.e. better to wrap text and shrink images than showing a horizontal scrollbar!
} else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if has_bar[d] {
content_max_size[d] = f32::INFINITY;
}
}
}
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
{
// Clip the content, but only when we really need to:
let clip_rect_margin = ui.visuals().clip_rect_margin;
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if has_bar[d] {
if state.content_is_too_large[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
}
if state.show_scroll[d] {
// Make sure content doesn't cover scroll bars
let tiny_gap = 1.0;
content_clip_rect.max[1 - d] =
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
}
} else {
// Nice handling of forced resizing beyond the possible:
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
}
}
// Make sure we din't accidentally expand the clip rect
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
content_ui.set_clip_rect(content_clip_rect);
}
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag());
if content_response.dragged() {
for d in 0..2 {
if has_bar[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
state.vel[d] = input.pointer.velocity()[d];
});
state.scroll_stuck_to_end[d] = false;
} else {
state.vel[d] = 0.0;
}
}
} else {
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input(|i| i.unstable_dt);
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
state.vel = Vec2::ZERO;
} else {
state.vel -= friction * state.vel.normalized();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset -= state.vel * dt;
ui.ctx().request_repaint();
}
}
}
Prepared {
id,
state,
has_bar,
auto_shrink,
current_bar_use,
always_show_scroll,
inner_rect,
content_ui,
viewport,
scrolling_enabled,
stick_to_end,
}
}
/// Show the [`ScrollArea`], and add the contents to the viewport.
///
/// If the inner area can be very long, consider using [`Self::show_rows`] instead.
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> ScrollAreaOutput<R> {
self.show_viewport_dyn(ui, Box::new(|ui, _viewport| add_contents(ui)))
}
/// Efficiently show only the visible part of a large number of rows.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let text_style = egui::TextStyle::Body;
/// let row_height = ui.text_style_height(&text_style);
/// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels.
/// let total_rows = 10_000;
/// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| {
/// for row in row_range {
/// let text = format!("Row {}/{}", row + 1, total_rows);
/// ui.label(text);
/// }
/// });
/// # });
/// ```
pub fn show_rows<R>(
self,
ui: &mut Ui,
row_height_sans_spacing: f32,
total_rows: usize,
add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
) -> ScrollAreaOutput<R> {
let spacing = ui.spacing().item_spacing;
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
self.show_viewport(ui, |ui, viewport| {
ui.set_height((row_height_with_spacing * total_rows as f32 - spacing.y).at_least(0.0));
let mut min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
let mut max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
if max_row > total_rows {
let diff = max_row.saturating_sub(min_row);
max_row = total_rows;
min_row = total_rows.saturating_sub(diff);
}
let y_min = ui.max_rect().top() + min_row as f32 * row_height_with_spacing;
let y_max = ui.max_rect().top() + max_row as f32 * row_height_with_spacing;
let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max);
ui.allocate_ui_at_rect(rect, |viewport_ui| {
viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs.
add_contents(viewport_ui, min_row..max_row)
})
.inner
})
}
/// This can be used to only paint the visible part of the contents.
///
/// `add_contents` is given the viewport rectangle, which is the relative view of the content.
/// So if the passed rect has min = zero, then show the top left content (the user has not scrolled).
pub fn show_viewport<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui, Rect) -> R,
) -> ScrollAreaOutput<R> {
self.show_viewport_dyn(ui, Box::new(add_contents))
}
fn show_viewport_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
) -> ScrollAreaOutput<R> {
let mut prepared = self.begin(ui);
let id = prepared.id;
let inner_rect = prepared.inner_rect;
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
let (content_size, state) = prepared.end(ui);
ScrollAreaOutput {
inner,
id,
state,
content_size,
inner_rect,
}
}
}
impl Prepared {
/// Returns content size and state
fn end(self, ui: &mut Ui) -> (Vec2, State) {
let Prepared {
id,
mut state,
inner_rect,
has_bar,
auto_shrink,
mut current_bar_use,
always_show_scroll,
content_ui,
viewport: _,
scrolling_enabled,
stick_to_end,
} = self;
let content_size = content_ui.min_size();
for d in 0..2 {
if has_bar[d] {
// We take the scroll target so only this ScrollArea will use it:
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if let Some((scroll, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
let visible_range = min..=min + clip_rect.size()[d];
let start = *scroll.start();
let end = *scroll.end();
let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d];
let delta = if let Some(align) = align {
let center_factor = align.to_factor();
let offset =
lerp(scroll, center_factor) - lerp(visible_range, center_factor);
// Depending on the alignment we need to add or subtract the spacing
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
offset + spacing - state.offset[d]
} else if start < clip_start && end < clip_end {
-(clip_start - start + spacing).min(clip_end - end - spacing)
} else if end > clip_end && start > clip_start {
(end - clip_end + spacing).min(start - clip_start - spacing)
} else {
// Ui is already in view, no need to adjust scroll.
0.0
};
if delta != 0.0 {
state.offset[d] += delta;
ui.ctx().request_repaint();
}
}
}
}
let inner_rect = {
// At this point this is the available size for the inner rect.
let mut inner_size = inner_rect.size();
for d in 0..2 {
inner_size[d] = match (has_bar[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
(false, false) => inner_size[d].max(content_size[d]), // Expand to fit content
};
}
Rect::from_min_size(inner_rect.min, inner_size)
};
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = [
content_size.x > inner_rect.width(),
content_size.y > inner_rect.height(),
];
let max_offset = content_size - inner_rect.size();
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
for d in 0..2 {
if has_bar[d] {
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
if scrolling_up || scrolling_down {
state.offset[d] -= scroll_delta[d];
// Clear scroll delta so no parent scroll will use it.
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
state.scroll_stuck_to_end[d] = false;
}
}
}
}
let show_scroll_this_frame = [
content_is_too_large[0] || always_show_scroll,
content_is_too_large[1] || always_show_scroll,
];
let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui);
// Avoid frame delay; start showing scroll bar right away:
if show_scroll_this_frame[0] && current_bar_use.y <= 0.0 {
current_bar_use.y = max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), true);
}
if show_scroll_this_frame[1] && current_bar_use.x <= 0.0 {
current_bar_use.x = max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), true);
}
for d in 0..2 {
let animation_t = current_bar_use[1 - d] / max_scroll_bar_width;
if animation_t == 0.0 {
continue;
}
// margin on either side of the scroll bar
let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin;
let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin;
let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1)
let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1)
let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1)
let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1)
if ui.clip_rect().max[1 - d] < max_cross + outer_margin {
// Move the scrollbar so it is visible. This is needed in some cases.
// For instance:
// * When we have a vertical-only scroll area in a top level panel,
// and that panel is not wide enough for the contents.
// * When one ScrollArea is nested inside another, and the outer
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
// is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
let width = max_cross - min_cross;
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
min_cross = max_cross - width;
}
let outer_scroll_rect = if d == 0 {
Rect::from_min_max(
pos2(inner_rect.left(), min_cross),
pos2(inner_rect.right(), max_cross),
)
} else {
Rect::from_min_max(
pos2(min_cross, inner_rect.top()),
pos2(max_cross, inner_rect.bottom()),
)
};
// maybe force increase in offset to keep scroll stuck to end position
if stick_to_end[d] && state.scroll_stuck_to_end[d] {
state.offset[d] = content_size[d] - inner_rect.size()[d];
}
let from_content =
|content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main);
let handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), min_cross),
pos2(from_content(state.offset.x + inner_rect.width()), max_cross),
)
} else {
Rect::from_min_max(
pos2(min_cross, from_content(state.offset.y)),
pos2(
max_cross,
from_content(state.offset.y + inner_rect.height()),
),
)
};
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
Sense::click_and_drag()
} else {
Sense::hover()
};
let response = ui.interact(outer_scroll_rect, interact_id, sense);
if let Some(pointer_pos) = response.interact_pointer_pos() {
let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d]
.get_or_insert_with(|| {
if handle_rect.contains(pointer_pos) {
pointer_pos[d] - handle_rect.min[d]
} else {
let handle_top_pos_at_bottom = max_main - handle_rect.size()[d];
// Calculate the new handle top position, centering the handle on the mouse.
let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0)
.clamp(min_main, handle_top_pos_at_bottom);
pointer_pos[d] - new_handle_top_pos
}
});
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]);
// some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false;
} else {
state.scroll_start_offset_from_top_left[d] = None;
}
let unbounded_offset = state.offset[d];
state.offset[d] = state.offset[d].max(0.0);
state.offset[d] = state.offset[d].min(max_offset[d]);
if state.offset[d] != unbounded_offset {
state.vel[d] = 0.0;
}
if ui.is_rect_visible(outer_scroll_rect) {
// Avoid frame-delay by calculating a new handle rect:
let mut handle_rect = if d == 0 {
Rect::from_min_max(
pos2(from_content(state.offset.x), min_cross),
pos2(from_content(state.offset.x + inner_rect.width()), max_cross),
)
} else {
Rect::from_min_max(
pos2(min_cross, from_content(state.offset.y)),
pos2(
max_cross,
from_content(state.offset.y + inner_rect.height()),
),
)
};
let min_handle_size = ui.spacing().scroll_handle_min_length;
if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size(
handle_rect.center(),
if d == 0 {
vec2(min_handle_size, handle_rect.size().y)
} else {
vec2(handle_rect.size().x, min_handle_size)
},
);
}
let visuals = if scrolling_enabled {
ui.style().interact(&response)
} else {
&ui.style().visuals.widgets.inactive
};
ui.painter().add(epaint::Shape::rect_filled(
outer_scroll_rect,
visuals.rounding,
ui.visuals().extreme_bg_color,
));
ui.painter().add(epaint::Shape::rect_filled(
handle_rect,
visuals.rounding,
visuals.bg_fill,
));
}
}
ui.advance_cursor_after_rect(outer_rect);
if show_scroll_this_frame != state.show_scroll {
ui.ctx().request_repaint();
}
let available_offset = content_size - inner_rect.size();
state.offset = state.offset.min(available_offset);
state.offset = state.offset.max(Vec2::ZERO);
// Is scroll handle at end of content, or is there no scrollbar
// yet (not enough content), but sticking is requested? If so, enter sticky mode.
// Only has an effect if stick_to_end is enabled but we save in
// state anyway so that entering sticky mode at an arbitrary time
// has appropriate effect.
state.scroll_stuck_to_end = [
(state.offset[0] == available_offset[0])
|| (self.stick_to_end[0] && available_offset[0] < 0.),
(state.offset[1] == available_offset[1])
|| (self.stick_to_end[1] && available_offset[1] < 0.),
];
state.show_scroll = show_scroll_this_frame;
state.content_is_too_large = content_is_too_large;
state.store(ui.ctx(), id);
(content_size, state)
}
}
/// Width of a vertical scrollbar, or height of a horizontal scroll bar
fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 {
ui.spacing().scroll_bar_inner_margin
+ ui.spacing().scroll_bar_width
+ ui.spacing().scroll_bar_outer_margin
}

File diff suppressed because it is too large Load diff

View file

@ -1,910 +0,0 @@
//! The input needed by egui.
use crate::emath::*;
/// What the integrations provides to egui at the start of each frame.
///
/// Set the values that make sense, leave the rest at their `Default::default()`.
///
/// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
///
/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RawInput {
/// Position and size of the area that egui should use, in points.
/// Usually you would set this to
///
/// `Some(Rect::from_pos_size(Default::default(), screen_size_in_points))`.
///
/// but you could also constrain egui to some smaller portion of your window if you like.
///
/// `None` will be treated as "same as last frame", with the default being a very big area.
pub screen_rect: Option<Rect>,
/// Also known as device pixel ratio, > 1 for high resolution screens.
/// If text looks blurry you probably forgot to set this.
/// Set this the first frame, whenever it changes, or just on every frame.
pub pixels_per_point: Option<f32>,
/// Maximum size of one side of the font texture.
///
/// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`.
///
/// The default is a very small (but very portable) 2048.
pub max_texture_side: Option<usize>,
/// Monotonically increasing time, in seconds. Relative to whatever. Used for animations.
/// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds).
pub time: Option<f64>,
/// Should be set to the expected time between frames when painting at vsync speeds.
/// The default for this is 1/60.
/// Can safely be left at its default value.
pub predicted_dt: f32,
/// Which modifier keys are down at the start of the frame?
pub modifiers: Modifiers,
/// In-order events received this frame.
///
/// There is currently no way to know if egui handles a particular event,
/// but you can check if egui is using the keyboard with [`crate::Context::wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::is_using_pointer`].
pub events: Vec<Event>,
/// Dragged files hovering over egui.
pub hovered_files: Vec<HoveredFile>,
/// Dragged files dropped into egui.
///
/// Note: when using `eframe` on Windows you need to enable
/// drag-and-drop support using `eframe::NativeOptions`.
pub dropped_files: Vec<DroppedFile>,
/// The window has the keyboard focus (i.e. is receiving key presses).
pub has_focus: bool,
}
impl Default for RawInput {
fn default() -> Self {
Self {
screen_rect: None,
pixels_per_point: None,
max_texture_side: None,
time: None,
predicted_dt: 1.0 / 60.0,
modifiers: Modifiers::default(),
events: vec![],
hovered_files: Default::default(),
dropped_files: Default::default(),
has_focus: true, // integrations opt into global focus tracking
}
}
}
impl RawInput {
/// Helper: move volatile (deltas and events), clone the rest.
///
/// * [`Self::hovered_files`] is cloned.
/// * [`Self::dropped_files`] is moved.
pub fn take(&mut self) -> RawInput {
RawInput {
screen_rect: self.screen_rect.take(),
pixels_per_point: self.pixels_per_point.take(),
max_texture_side: self.max_texture_side.take(),
time: self.time.take(),
predicted_dt: self.predicted_dt,
modifiers: self.modifiers,
events: std::mem::take(&mut self.events),
hovered_files: self.hovered_files.clone(),
dropped_files: std::mem::take(&mut self.dropped_files),
has_focus: self.has_focus,
}
}
/// Add on new input.
pub fn append(&mut self, newer: Self) {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
mut events,
mut hovered_files,
mut dropped_files,
has_focus,
} = newer;
self.screen_rect = screen_rect.or(self.screen_rect);
self.pixels_per_point = pixels_per_point.or(self.pixels_per_point);
self.max_texture_side = max_texture_side.or(self.max_texture_side);
self.time = time; // use latest time
self.predicted_dt = predicted_dt; // use latest dt
self.modifiers = modifiers; // use latest
self.events.append(&mut events);
self.hovered_files.append(&mut hovered_files);
self.dropped_files.append(&mut dropped_files);
self.has_focus = has_focus;
}
}
/// A file about to be dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct HoveredFile {
/// Set by the `egui-winit` backend.
pub path: Option<std::path::PathBuf>,
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
pub mime: String,
}
/// A file dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DroppedFile {
/// Set by the `egui-winit` backend.
pub path: Option<std::path::PathBuf>,
/// Name of the file. Set by the `eframe` web backend.
pub name: String,
/// Set by the `eframe` web backend.
pub last_modified: Option<std::time::SystemTime>,
/// Set by the `eframe` web backend.
pub bytes: Option<std::sync::Arc<[u8]>>,
}
/// An input event generated by the integration.
///
/// This only covers events that egui cares about.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Event {
/// The integration detected a "copy" event (e.g. Cmd+C).
Copy,
/// The integration detected a "cut" event (e.g. Cmd+X).
Cut,
/// The integration detected a "paste" event (e.g. Cmd+V).
Paste(String),
/// Text input, e.g. via keyboard.
///
/// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]).
Text(String),
/// A key was pressed or released.
Key {
key: Key,
/// Was it pressed or released?
pressed: bool,
/// If this is a `pressed` event, is it a key-repeat?
///
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
/// Sometimes you will want to ignore such events, and this lets you do that.
///
/// egui will automatically detect such repeat events and mark them as such here.
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
repeat: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
},
/// The mouse or touch moved to a new place.
PointerMoved(Pos2),
/// A mouse button was pressed or released (or a touch started or stopped).
PointerButton {
/// Where is the pointer?
pos: Pos2,
/// What mouse button? For touches, use [`PointerButton::Primary`].
button: PointerButton,
/// Was it the button/touch pressed this frame, or released?
pressed: bool,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
},
/// The mouse left the screen, or the last/primary touch input disappeared.
///
/// This means there is no longer a cursor on the screen for hovering etc.
///
/// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`.
PointerGone,
/// How many points (logical pixels) the user scrolled.
///
/// The direction of the vector indicates how to move the _content_ that is being viewed.
/// So if you get positive values, the content being viewed should move to the right and down,
/// revealing new things to the left and up.
///
/// A positive X-value indicates the content is being moved right,
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
///
/// A positive Y-value indicates the content is being moved down,
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
///
/// Shift-scroll should result in horizontal scrolling (it is up to the integrations to do this).
Scroll(Vec2),
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
/// * `zoom = 1`: no change.
/// * `zoom < 1`: pinch together
/// * `zoom > 1`: pinch spread
Zoom(f32),
/// IME composition start.
CompositionStart,
/// A new IME candidate is being suggested.
CompositionUpdate(String),
/// IME composition ended with this final result.
CompositionEnd(String),
/// On touch screens, report this *in addition to*
/// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`]
Touch {
/// Hashed device identifier (if available; may be zero).
/// Can be used to separate touches from different devices.
device_id: TouchDeviceId,
/// Unique identifier of a finger/pen. Value is stable from touch down
/// to lift-up
id: TouchId,
/// One of: start move end cancel.
phase: TouchPhase,
/// Position of the touch (or where the touch was last detected)
pos: Pos2,
/// Describes how hard the touch device was pressed. May always be `0` if the platform does
/// not support pressure sensitivity.
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
force: f32,
},
/// An assistive technology (e.g. screen reader) requested an action.
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit::ActionRequest),
}
/// Mouse button (or similar for touch input)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum PointerButton {
/// The primary mouse button is usually the left one.
Primary = 0,
/// The secondary mouse button is usually the right one,
/// and most often used for context menus or other optional things.
Secondary = 1,
/// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel).
Middle = 2,
/// The first extra mouse button on some mice. In web typically corresponds to the Browser back button.
Extra1 = 3,
/// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button.
Extra2 = 4,
}
/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`].
pub const NUM_POINTER_BUTTONS: usize = 5;
/// State of the modifier keys. These must be fed to egui.
///
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
///
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
/// so those key presses are usually not reported to egui.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac).
pub alt: bool,
/// Either of the control keys are down.
/// When checking for keyboard shortcuts, consider using [`Self::command`] instead.
pub ctrl: bool,
/// Either of the shift keys are down.
pub shift: bool,
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
pub mac_cmd: bool,
/// On Windows and Linux, set this to the same value as `ctrl`.
/// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`).
/// This is so that egui can, for instance, select all text by checking for `command + A`
/// and it will work on both Mac and Windows.
pub command: bool,
}
impl Modifiers {
pub const NONE: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: false,
};
pub const ALT: Self = Self {
alt: true,
ctrl: false,
shift: false,
mac_cmd: false,
command: false,
};
pub const CTRL: Self = Self {
alt: false,
ctrl: true,
shift: false,
mac_cmd: false,
command: false,
};
pub const SHIFT: Self = Self {
alt: false,
ctrl: false,
shift: true,
mac_cmd: false,
command: false,
};
#[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"]
pub const ALT_SHIFT: Self = Self {
alt: true,
ctrl: false,
shift: true,
mac_cmd: false,
command: false,
};
/// The Mac ⌘ Command key
pub const MAC_CMD: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: true,
command: false,
};
/// On Mac: ⌘ Command key, elsewhere: Ctrl key
pub const COMMAND: Self = Self {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: true,
};
/// ```
/// # use egui::Modifiers;
/// assert_eq!(
/// Modifiers::CTRL | Modifiers::ALT,
/// Modifiers { ctrl: true, alt: true, ..Default::default() }
/// );
/// assert_eq!(
/// Modifiers::ALT.plus(Modifiers::CTRL),
/// Modifiers::CTRL.plus(Modifiers::ALT),
/// );
/// assert_eq!(
/// Modifiers::CTRL | Modifiers::ALT,
/// Modifiers::CTRL.plus(Modifiers::ALT),
/// );
/// ```
#[inline]
pub const fn plus(self, rhs: Self) -> Self {
Self {
alt: self.alt | rhs.alt,
ctrl: self.ctrl | rhs.ctrl,
shift: self.shift | rhs.shift,
mac_cmd: self.mac_cmd | rhs.mac_cmd,
command: self.command | rhs.command,
}
}
#[inline]
pub fn is_none(&self) -> bool {
self == &Self::default()
}
#[inline]
pub fn any(&self) -> bool {
!self.is_none()
}
/// Is shift the only pressed button?
#[inline]
pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command)
}
/// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed.
#[inline]
pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command
}
/// Check for equality but with proper handling of [`Self::command`].
///
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// ```
pub fn matches(&self, pattern: Modifiers) -> bool {
// alt and shift must always match the pattern:
if pattern.alt != self.alt || pattern.shift != self.shift {
return false;
}
if pattern.mac_cmd {
// Mac-specific match:
if !self.mac_cmd {
return false;
}
if pattern.ctrl != self.ctrl {
return false;
}
return true;
}
if !pattern.ctrl && !pattern.command {
// the pattern explicitly doesn't want any ctrl/command:
return !self.ctrl && !self.command;
}
// if the pattern is looking for command, then `ctrl` may or may not be set depending on platform.
// if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform.
if pattern.ctrl && !self.ctrl {
return false;
}
if pattern.command && !self.command {
return false;
}
true
}
}
impl std::ops::BitOr for Modifiers {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self {
self.plus(rhs)
}
}
// ----------------------------------------------------------------------------
/// Names of different modifier keys.
///
/// Used to name modifiers.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ModifierNames<'a> {
pub is_short: bool,
pub alt: &'a str,
pub ctrl: &'a str,
pub shift: &'a str,
pub mac_cmd: &'a str,
/// What goes between the names
pub concat: &'a str,
}
impl ModifierNames<'static> {
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font.
pub const SYMBOLS: Self = Self {
is_short: true,
alt: "",
ctrl: "^",
shift: "",
mac_cmd: "",
concat: "",
};
/// Alt, Ctrl, Shift, Cmd
pub const NAMES: Self = Self {
is_short: false,
alt: "Alt",
ctrl: "Ctrl",
shift: "Shift",
mac_cmd: "Cmd",
concat: "+",
};
}
impl<'a> ModifierNames<'a> {
pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String {
let mut s = String::new();
let mut append_if = |modifier_is_active, modifier_name| {
if modifier_is_active {
if !s.is_empty() {
s += self.concat;
}
s += modifier_name;
}
};
if is_mac {
append_if(modifiers.ctrl, self.ctrl);
append_if(modifiers.shift, self.shift);
append_if(modifiers.alt, self.alt);
append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd);
} else {
append_if(modifiers.ctrl || modifiers.command, self.ctrl);
append_if(modifiers.alt, self.alt);
append_if(modifiers.shift, self.shift);
}
s
}
}
// ----------------------------------------------------------------------------
/// Keyboard keys.
///
/// Includes all keys egui is interested in (such as `Home` and `End`)
/// plus a few that are useful for detecting keyboard shortcuts.
///
/// Many keys are omitted because they are not always physical keys (depending on keyboard language), e.g. `;` and `§`,
/// and are therefore unsuitable as keyboard shortcuts if you want your app to be portable.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Key {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
Escape,
Tab,
Backspace,
Enter,
Space,
Insert,
Delete,
Home,
End,
PageUp,
PageDown,
/// The virtual keycode for the Minus key.
Minus,
/// The virtual keycode for the Plus/Equals key.
PlusEquals,
/// Either from the main row or from the numpad.
Num0,
/// Either from the main row or from the numpad.
Num1,
/// Either from the main row or from the numpad.
Num2,
/// Either from the main row or from the numpad.
Num3,
/// Either from the main row or from the numpad.
Num4,
/// Either from the main row or from the numpad.
Num5,
/// Either from the main row or from the numpad.
Num6,
/// Either from the main row or from the numpad.
Num7,
/// Either from the main row or from the numpad.
Num8,
/// Either from the main row or from the numpad.
Num9,
A, // Used for cmd+A (select All)
B,
C, // |CMD COPY|
D, // |CMD BOOKMARK|
E, // |CMD SEARCH|
F, // |CMD FIND firefox & chrome|
G, // |CMD FIND chrome|
H, // |CMD History|
I, // italics
J, // |CMD SEARCH firefox/DOWNLOAD chrome|
K, // Used for ctrl+K (delete text after cursor)
L,
M,
N,
O, // |CMD OPEN|
P, // |CMD PRINT|
Q,
R, // |CMD REFRESH|
S, // |CMD SAVE|
T, // |CMD TAB|
U, // Used for ctrl+U (delete text before cursor)
V, // |CMD PASTE|
W, // Used for ctrl+W (delete previous word)
X, // |CMD CUT|
Y,
Z, // |CMD UNDO|
// The function keys:
F1,
F2,
F3,
F4,
F5, // |CMD REFRESH|
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
}
impl Key {
/// Emoji or name representing the key
pub fn symbol_or_name(self) -> &'static str {
// TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys).
// Before we do we must first make sure they are supported in `Fonts` though,
// so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something.
match self {
Key::ArrowDown => "",
Key::ArrowLeft => "",
Key::ArrowRight => "",
Key::ArrowUp => "",
Key::Minus => "-",
Key::PlusEquals => "+",
_ => self.name(),
}
}
/// Human-readable English name.
pub fn name(self) -> &'static str {
match self {
Key::ArrowDown => "Down",
Key::ArrowLeft => "Left",
Key::ArrowRight => "Right",
Key::ArrowUp => "Up",
Key::Escape => "Escape",
Key::Tab => "Tab",
Key::Backspace => "Backspace",
Key::Enter => "Enter",
Key::Space => "Space",
Key::Insert => "Insert",
Key::Delete => "Delete",
Key::Home => "Home",
Key::End => "End",
Key::PageUp => "PageUp",
Key::PageDown => "PageDown",
Key::Minus => "Minus",
Key::PlusEquals => "Plus",
Key::Num0 => "0",
Key::Num1 => "1",
Key::Num2 => "2",
Key::Num3 => "3",
Key::Num4 => "4",
Key::Num5 => "5",
Key::Num6 => "6",
Key::Num7 => "7",
Key::Num8 => "8",
Key::Num9 => "9",
Key::A => "A",
Key::B => "B",
Key::C => "C",
Key::D => "D",
Key::E => "E",
Key::F => "F",
Key::G => "G",
Key::H => "H",
Key::I => "I",
Key::J => "J",
Key::K => "K",
Key::L => "L",
Key::M => "M",
Key::N => "N",
Key::O => "O",
Key::P => "P",
Key::Q => "Q",
Key::R => "R",
Key::S => "S",
Key::T => "T",
Key::U => "U",
Key::V => "V",
Key::W => "W",
Key::X => "X",
Key::Y => "Y",
Key::Z => "Z",
Key::F1 => "F1",
Key::F2 => "F2",
Key::F3 => "F3",
Key::F4 => "F4",
Key::F5 => "F5",
Key::F6 => "F6",
Key::F7 => "F7",
Key::F8 => "F8",
Key::F9 => "F9",
Key::F10 => "F10",
Key::F11 => "F11",
Key::F12 => "F12",
Key::F13 => "F13",
Key::F14 => "F14",
Key::F15 => "F15",
Key::F16 => "F16",
Key::F17 => "F17",
Key::F18 => "F18",
Key::F19 => "F19",
Key::F20 => "F20",
}
}
}
// ----------------------------------------------------------------------------
/// A keyboard shortcut, e.g. `Ctrl+Alt+W`.
///
/// Can be used with [`crate::InputState::consume_shortcut`]
/// and [`crate::Context::format_shortcut`].
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct KeyboardShortcut {
pub modifiers: Modifiers,
pub key: Key,
}
impl KeyboardShortcut {
pub const fn new(modifiers: Modifiers, key: Key) -> Self {
Self { modifiers, key }
}
pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String {
let mut s = names.format(&self.modifiers, is_mac);
if !s.is_empty() {
s += names.concat;
}
if names.is_short {
s += self.key.symbol_or_name();
} else {
s += self.key.name();
}
s
}
}
#[test]
fn format_kb_shortcut() {
let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F);
assert_eq!(
cmd_shift_f.format(&ModifierNames::NAMES, false),
"Ctrl+Shift+F"
);
assert_eq!(
cmd_shift_f.format(&ModifierNames::NAMES, true),
"Shift+Cmd+F"
);
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F");
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
}
// ----------------------------------------------------------------------------
impl RawInput {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
screen_rect,
pixels_per_point,
max_texture_side,
time,
predicted_dt,
modifiers,
events,
hovered_files,
dropped_files,
has_focus,
} = self;
ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))
.on_hover_text(
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
);
ui.label(format!("max_texture_side: {:?}", max_texture_side));
if let Some(time) = time {
ui.label(format!("time: {:.3} s", time));
} else {
ui.label("time: None");
}
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.label(format!("has_focus: {}", has_focus));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
.on_hover_text("key presses etc");
});
}
}
/// this is a `u64` as values of this kind can always be obtained by hashing
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TouchDeviceId(pub u64);
/// Unique identification of a touch occurrence (finger or pen or …).
/// A Touch ID is valid until the finger is lifted.
/// A new ID is used for the next touch.
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TouchId(pub u64);
/// In what phase a touch event is in.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TouchPhase {
/// User just placed a touch point on the touch surface
Start,
/// User moves a touch point along the surface. This event is also sent when
/// any attributes (position, force, …) of the touch point change.
Move,
/// User lifted the finger or pen from the surface, or slid off the edge of
/// the surface
End,
/// Touch operation has been disrupted by something (various reasons are possible,
/// maybe a pop-up alert or any other kind of interruption which may not have
/// been intended by the user)
Cancel,
}
impl From<u64> for TouchId {
fn from(id: u64) -> Self {
Self(id)
}
}
impl From<i32> for TouchId {
fn from(id: i32) -> Self {
Self(id as u64)
}
}
impl From<u32> for TouchId {
fn from(id: u32) -> Self {
Self(id as u64)
}
}

View file

@ -1,629 +0,0 @@
//! All the data egui returns to the backend at the end of each frame.
use crate::WidgetType;
/// What egui emits each frame from [`crate::Context::run`].
///
/// The backend should use this.
#[derive(Clone, Default, PartialEq)]
pub struct FullOutput {
/// Non-rendering related output.
pub platform_output: PlatformOutput,
/// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame).
///
/// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`.
///
/// If `Duration` is greater than zero, egui wants to be repainted at or before the specified
/// duration elapses. when in reactive mode, egui spends forever waiting for input and only then,
/// will it repaint itself. this can be used to make sure that backend will only wait for a
/// specified amount of time, and repaint egui without any new input.
pub repaint_after: std::time::Duration,
/// Texture changes since last frame (including the font texture).
///
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting,
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
pub textures_delta: epaint::textures::TexturesDelta,
/// What to paint.
///
/// You can use [`crate::Context::tessellate`] to turn this into triangles.
pub shapes: Vec<epaint::ClippedShape>,
}
impl FullOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
let Self {
platform_output,
repaint_after,
textures_delta,
shapes,
} = newer;
self.platform_output.append(platform_output);
self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint
self.textures_delta.append(textures_delta);
self.shapes = shapes; // Only paint the latest
}
}
/// The non-rendering part of what egui emits each frame.
///
/// You can access (and modify) this with [`crate::Context::output`].
///
/// The backend should use this.
#[derive(Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PlatformOutput {
/// Set the cursor to this icon.
pub cursor_icon: CursorIcon,
/// If set, open this url.
pub open_url: Option<OpenUrl>,
/// If set, put this text in the system clipboard. Ignore if empty.
///
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
/// }
/// # });
/// ```
pub copied_text: String,
/// Events that may be useful to e.g. a screen reader.
pub events: Vec<OutputEvent>,
/// Is there a mutable [`TextEdit`](crate::TextEdit) under the cursor?
/// Use by `eframe` web to show/hide mobile keyboard and IME agent.
pub mutable_text_under_cursor: bool,
/// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>,
#[cfg(feature = "accesskit")]
pub accesskit_update: Option<accesskit::TreeUpdate>,
}
impl PlatformOutput {
/// Open the given url in a web browser.
/// If egui is running in a browser, the same tab will be reused.
pub fn open_url(&mut self, url: impl ToString) {
self.open_url = Some(OpenUrl::same_tab(url));
}
/// This can be used by a text-to-speech system to describe the events (if any).
pub fn events_description(&self) -> String {
// only describe last event:
if let Some(event) = self.events.iter().rev().next() {
match event {
OutputEvent::Clicked(widget_info)
| OutputEvent::DoubleClicked(widget_info)
| OutputEvent::TripleClicked(widget_info)
| OutputEvent::FocusGained(widget_info)
| OutputEvent::TextSelectionChanged(widget_info)
| OutputEvent::ValueChanged(widget_info) => {
return widget_info.description();
}
}
}
Default::default()
}
/// Add on new output.
pub fn append(&mut self, newer: Self) {
let Self {
cursor_icon,
open_url,
copied_text,
mut events,
mutable_text_under_cursor,
text_cursor_pos,
#[cfg(feature = "accesskit")]
accesskit_update,
} = newer;
self.cursor_icon = cursor_icon;
if open_url.is_some() {
self.open_url = open_url;
}
if !copied_text.is_empty() {
self.copied_text = copied_text;
}
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
#[cfg(feature = "accesskit")]
{
// egui produces a complete AccessKit tree for each frame,
// so overwrite rather than appending.
self.accesskit_update = accesskit_update;
}
}
/// Take everything ephemeral (everything except `cursor_icon` currently)
pub fn take(&mut self) -> Self {
let taken = std::mem::take(self);
self.cursor_icon = taken.cursor_icon; // eveything else is ephemeral
taken
}
}
/// What URL to open, and how.
#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct OpenUrl {
pub url: String,
/// If `true`, open the url in a new tab.
/// If `false` open it in the same tab.
/// Only matters when in a web browser.
pub new_tab: bool,
}
impl OpenUrl {
#[allow(clippy::needless_pass_by_value)]
pub fn same_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
new_tab: false,
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn new_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
new_tab: true,
}
}
}
/// A mouse cursor icon.
///
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
///
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum CursorIcon {
/// Normal cursor icon, whatever that is.
Default,
/// Show no cursor
None,
// ------------------------------------
// Links and status:
/// A context menu is available
ContextMenu,
/// Question mark
Help,
/// Pointing hand, used for e.g. web links
PointingHand,
/// Shows that processing is being done, but that the program is still interactive.
Progress,
/// Not yet ready, try later.
Wait,
// ------------------------------------
// Selection:
/// Hover a cell in a table
Cell,
/// For precision work
Crosshair,
/// Text caret, e.g. "Click here to edit text"
Text,
/// Vertical text caret, e.g. "Click here to edit vertical text"
VerticalText,
// ------------------------------------
// Drag-and-drop:
/// Indicated an alias, e.g. a shortcut
Alias,
/// Indicate that a copy will be made
Copy,
/// Omnidirectional move icon (e.g. arrows in all cardinal directions)
Move,
/// Can't drop here
NoDrop,
/// Forbidden
NotAllowed,
/// The thing you are hovering can be grabbed
Grab,
/// You are grabbing the thing you are hovering
Grabbing,
// ------------------------------------
/// Something can be scrolled in any direction (panned).
AllScroll,
// ------------------------------------
// Resizing in two directions:
/// Horizontal resize `-` to make something wider or more narrow (left to/from right)
ResizeHorizontal,
/// Diagonal resize `/` (right-up to/from left-down)
ResizeNeSw,
/// Diagonal resize `\` (left-up to/from right-down)
ResizeNwSe,
/// Vertical resize `|` (up-down or down-up)
ResizeVertical,
// ------------------------------------
// Resizing in one direction:
/// Resize something rightwards (e.g. when dragging the right-most edge of something)
ResizeEast,
/// Resize something down and right (e.g. when dragging the bottom-right corner of something)
ResizeSouthEast,
/// Resize something downwards (e.g. when dragging the bottom edge of something)
ResizeSouth,
/// Resize something down and left (e.g. when dragging the bottom-left corner of something)
ResizeSouthWest,
/// Resize something leftwards (e.g. when dragging the left edge of something)
ResizeWest,
/// Resize something up and left (e.g. when dragging the top-left corner of something)
ResizeNorthWest,
/// Resize something up (e.g. when dragging the top edge of something)
ResizeNorth,
/// Resize something up and right (e.g. when dragging the top-right corner of something)
ResizeNorthEast,
// ------------------------------------
/// Resize a column
ResizeColumn,
/// Resize a row
ResizeRow,
// ------------------------------------
// Zooming:
/// Enhance!
ZoomIn,
/// Let's get a better overview
ZoomOut,
}
impl CursorIcon {
pub const ALL: [CursorIcon; 35] = [
CursorIcon::Default,
CursorIcon::None,
CursorIcon::ContextMenu,
CursorIcon::Help,
CursorIcon::PointingHand,
CursorIcon::Progress,
CursorIcon::Wait,
CursorIcon::Cell,
CursorIcon::Crosshair,
CursorIcon::Text,
CursorIcon::VerticalText,
CursorIcon::Alias,
CursorIcon::Copy,
CursorIcon::Move,
CursorIcon::NoDrop,
CursorIcon::NotAllowed,
CursorIcon::Grab,
CursorIcon::Grabbing,
CursorIcon::AllScroll,
CursorIcon::ResizeHorizontal,
CursorIcon::ResizeNeSw,
CursorIcon::ResizeNwSe,
CursorIcon::ResizeVertical,
CursorIcon::ResizeEast,
CursorIcon::ResizeSouthEast,
CursorIcon::ResizeSouth,
CursorIcon::ResizeSouthWest,
CursorIcon::ResizeWest,
CursorIcon::ResizeNorthWest,
CursorIcon::ResizeNorth,
CursorIcon::ResizeNorthEast,
CursorIcon::ResizeColumn,
CursorIcon::ResizeRow,
CursorIcon::ZoomIn,
CursorIcon::ZoomOut,
];
}
impl Default for CursorIcon {
fn default() -> Self {
Self::Default
}
}
/// Things that happened during this frame that the integration may be interested in.
///
/// In particular, these events may be useful for accessability, i.e. for screen readers.
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum OutputEvent {
/// A widget was clicked.
Clicked(WidgetInfo),
/// A widget was double-clicked.
DoubleClicked(WidgetInfo),
/// A widget was triple-clicked.
TripleClicked(WidgetInfo),
/// A widget gained keyboard focus (by tab key).
FocusGained(WidgetInfo),
/// Text selection was updated.
TextSelectionChanged(WidgetInfo),
/// A widget's value changed.
ValueChanged(WidgetInfo),
}
impl OutputEvent {
pub fn widget_info(&self) -> &WidgetInfo {
match self {
OutputEvent::Clicked(info)
| OutputEvent::DoubleClicked(info)
| OutputEvent::TripleClicked(info)
| OutputEvent::FocusGained(info)
| OutputEvent::TextSelectionChanged(info)
| OutputEvent::ValueChanged(info) => info,
}
}
}
impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi),
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
}
}
}
/// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`].
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetInfo {
/// The type of widget this is.
pub typ: WidgetType,
/// Whether the widget is enabled.
pub enabled: bool,
/// The text on labels, buttons, checkboxes etc.
pub label: Option<String>,
/// The contents of some editable text (for [`TextEdit`](crate::TextEdit) fields).
pub current_text_value: Option<String>,
/// The previous text value.
pub prev_text_value: Option<String>,
/// The current value of checkboxes and radio buttons.
pub selected: Option<bool>,
/// The current value of sliders etc.
pub value: Option<f64>,
/// Selected range of characters in [`Self::current_text_value`].
pub text_selection: Option<std::ops::RangeInclusive<usize>>,
}
impl std::fmt::Debug for WidgetInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
typ,
enabled,
label,
current_text_value: text_value,
prev_text_value,
selected,
value,
text_selection,
} = self;
let mut s = f.debug_struct("WidgetInfo");
s.field("typ", typ);
s.field("enabled", enabled);
if let Some(label) = label {
s.field("label", label);
}
if let Some(text_value) = text_value {
s.field("text_value", text_value);
}
if let Some(prev_text_value) = prev_text_value {
s.field("prev_text_value", prev_text_value);
}
if let Some(selected) = selected {
s.field("selected", selected);
}
if let Some(value) = value {
s.field("value", value);
}
if let Some(text_selection) = text_selection {
s.field("text_selection", text_selection);
}
s.finish()
}
}
impl WidgetInfo {
pub fn new(typ: WidgetType) -> Self {
Self {
typ,
enabled: true,
label: None,
current_text_value: None,
prev_text_value: None,
selected: None,
value: None,
text_selection: None,
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn labeled(typ: WidgetType, label: impl ToString) -> Self {
Self {
label: Some(label.to_string()),
..Self::new(typ)
}
}
/// checkboxes, radio-buttons etc
#[allow(clippy::needless_pass_by_value)]
pub fn selected(typ: WidgetType, selected: bool, label: impl ToString) -> Self {
Self {
label: Some(label.to_string()),
selected: Some(selected),
..Self::new(typ)
}
}
pub fn drag_value(value: f64) -> Self {
Self {
value: Some(value),
..Self::new(WidgetType::DragValue)
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn slider(value: f64, label: impl ToString) -> Self {
let label = label.to_string();
Self {
label: if label.is_empty() { None } else { Some(label) },
value: Some(value),
..Self::new(WidgetType::Slider)
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn text_edit(prev_text_value: impl ToString, text_value: impl ToString) -> Self {
let text_value = text_value.to_string();
let prev_text_value = prev_text_value.to_string();
let prev_text_value = if text_value == prev_text_value {
None
} else {
Some(prev_text_value)
};
Self {
current_text_value: Some(text_value),
prev_text_value,
..Self::new(WidgetType::TextEdit)
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn text_selection_changed(
text_selection: std::ops::RangeInclusive<usize>,
current_text_value: impl ToString,
) -> Self {
Self {
text_selection: Some(text_selection),
current_text_value: Some(current_text_value.to_string()),
..Self::new(WidgetType::TextEdit)
}
}
/// This can be used by a text-to-speech system to describe the widget.
pub fn description(&self) -> String {
let Self {
typ,
enabled,
label,
current_text_value: text_value,
prev_text_value: _,
selected,
value,
text_selection: _,
} = self;
// TODO(emilk): localization
let widget_type = match typ {
WidgetType::Link => "link",
WidgetType::TextEdit => "text edit",
WidgetType::Button => "button",
WidgetType::Checkbox => "checkbox",
WidgetType::RadioButton => "radio",
WidgetType::SelectableLabel => "selectable",
WidgetType::ComboBox => "combo",
WidgetType::Slider => "slider",
WidgetType::DragValue => "drag value",
WidgetType::ColorButton => "color button",
WidgetType::ImageButton => "image button",
WidgetType::CollapsingHeader => "collapsing header",
WidgetType::Label | WidgetType::Other => "",
};
let mut description = widget_type.to_owned();
if let Some(selected) = selected {
if *typ == WidgetType::Checkbox {
let state = if *selected { "checked" } else { "unchecked" };
description = format!("{} {}", state, description);
} else {
description += if *selected { "selected" } else { "" };
};
}
if let Some(label) = label {
description = format!("{}: {}", label, description);
}
if typ == &WidgetType::TextEdit {
let text;
if let Some(text_value) = text_value {
if text_value.is_empty() {
text = "blank".into();
} else {
text = text_value.to_string();
}
} else {
text = "blank".into();
}
description = format!("{}: {}", text, description);
}
if let Some(value) = value {
description += " ";
description += &value.to_string();
}
if !enabled {
description += ": disabled";
}
description.trim().to_owned()
}
}

View file

@ -1,173 +0,0 @@
use std::ops::RangeInclusive;
use crate::{id::IdSet, *};
#[derive(Clone, Copy, Debug)]
pub(crate) struct TooltipFrameState {
pub common_id: Id,
pub rect: Rect,
pub count: usize,
}
#[cfg(feature = "accesskit")]
#[derive(Clone)]
pub(crate) struct AccessKitFrameState {
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
pub(crate) parent_stack: Vec<Id>,
}
/// State that is collected during a frame and then cleared.
/// Short-term (single frame) memory.
#[derive(Clone)]
pub(crate) struct FrameState {
/// All [`Id`]s that were used this frame.
/// Used to debug [`Id`] clashes of widgets.
pub(crate) used_ids: IdMap<Rect>,
/// Starts off as the screen_rect, shrinks as panels are added.
/// The [`CentralPanel`] does not change this.
/// This is the area available to Window's.
pub(crate) available_rect: Rect,
/// Starts off as the screen_rect, shrinks as panels are added.
/// The [`CentralPanel`] retracts from this.
pub(crate) unused_rect: Rect,
/// How much space is used by panels.
pub(crate) used_by_panels: Rect,
/// If a tooltip has been shown this frame, where was it?
/// This is used to prevent multiple tooltips to cover each other.
/// Initialized to `None` at the start of each frame.
pub(crate) tooltip_state: Option<TooltipFrameState>,
/// Set to [`InputState::scroll_delta`] on the start of each frame.
///
/// Cleared by the first [`ScrollArea`] that makes use of it.
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?
/// horizontal, vertical
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
#[cfg(feature = "accesskit")]
pub(crate) accesskit_state: Option<AccessKitFrameState>,
/// Highlight these widgets this next frame. Read from this.
pub(crate) highlight_this_frame: IdSet,
/// Highlight these widgets the next frame. Write to this.
pub(crate) highlight_next_frame: IdSet,
}
impl Default for FrameState {
fn default() -> Self {
Self {
used_ids: Default::default(),
available_rect: Rect::NAN,
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
tooltip_state: None,
scroll_delta: Vec2::ZERO,
scroll_target: [None, None],
#[cfg(feature = "accesskit")]
accesskit_state: None,
highlight_this_frame: Default::default(),
highlight_next_frame: Default::default(),
}
}
}
impl FrameState {
pub(crate) fn begin_frame(&mut self, input: &InputState) {
let Self {
used_ids,
available_rect,
unused_rect,
used_by_panels,
tooltip_state,
scroll_delta,
scroll_target,
#[cfg(feature = "accesskit")]
accesskit_state,
highlight_this_frame,
highlight_next_frame,
} = self;
used_ids.clear();
*available_rect = input.screen_rect();
*unused_rect = input.screen_rect();
*used_by_panels = Rect::NOTHING;
*tooltip_state = None;
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];
#[cfg(feature = "accesskit")]
{
*accesskit_state = None;
}
*highlight_this_frame = std::mem::take(highlight_next_frame);
}
/// How much space is still available after panels has been added.
/// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
/// This is also the area to which windows are constrained.
pub(crate) fn available_rect(&self) -> Rect {
crate::egui_assert!(
self.available_rect.is_finite(),
"Called `available_rect()` before `Context::run()`"
);
self.available_rect
}
/// Shrink `available_rect`.
pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
crate::egui_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching left panel. You must not create a panel from within another panel."
);
self.available_rect.min.x = panel_rect.max.x;
self.unused_rect.min.x = panel_rect.max.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
}
/// Shrink `available_rect`.
pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
crate::egui_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching right panel. You must not create a panel from within another panel."
);
self.available_rect.max.x = panel_rect.min.x;
self.unused_rect.max.x = panel_rect.min.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
}
/// Shrink `available_rect`.
pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
crate::egui_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching top panel. You must not create a panel from within another panel."
);
self.available_rect.min.y = panel_rect.max.y;
self.unused_rect.min.y = panel_rect.max.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
}
/// Shrink `available_rect`.
pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
crate::egui_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching bottom panel. You must not create a panel from within another panel."
);
self.available_rect.max.y = panel_rect.min.y;
self.unused_rect.max.y = panel_rect.min.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
}
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.
self.unused_rect = Rect::NOTHING; // Nothing left unused after this
self.used_by_panels = self.used_by_panels.union(panel_rect);
}
}

View file

@ -1,117 +0,0 @@
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`].
//!
use crate::*;
/// The suggested keyboard shortcuts for global gui zooming.
pub mod kb_shortcuts {
use super::*;
pub const ZOOM_IN: KeyboardShortcut =
KeyboardShortcut::new(Modifiers::COMMAND, Key::PlusEquals);
pub const ZOOM_OUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Minus);
pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0);
}
/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing
/// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser.
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// egui::gui_zoom::zoom_with_keyboard_shortcuts(
/// ctx,
/// frame.info().native_pixels_per_point,
/// );
/// }
/// ```
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
if let Some(native_pixels_per_point) = native_pixels_per_point {
ctx.set_pixels_per_point(native_pixels_per_point);
}
} else {
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
zoom_in(ctx);
}
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
zoom_out(ctx);
}
}
}
const MIN_PIXELS_PER_POINT: f32 = 0.2;
const MAX_PIXELS_PER_POINT: f32 = 4.0;
/// Make everything larger.
pub fn zoom_in(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point += 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Make everything smaller.
pub fn zoom_out(ctx: &Context) {
let mut pixels_per_point = ctx.pixels_per_point();
pixels_per_point -= 0.1;
pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT);
pixels_per_point = (pixels_per_point * 10.).round() / 10.;
ctx.set_pixels_per_point(pixels_per_point);
}
/// Show buttons for zooming the ui.
///
/// This is meant to be called from within a menu (See [`Ui::menu_button`]).
///
/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as:
/// ```ignore
/// // On web, the browser controls the gui zoom.
/// if !frame.is_web() {
/// ui.menu_button("View", |ui| {
/// egui::gui_zoom::zoom_menu_buttons(
/// ui,
/// frame.info().native_pixels_per_point,
/// );
/// });
/// }
/// ```
pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option<f32>) {
if ui
.add_enabled(
ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT,
Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)),
)
.clicked()
{
zoom_in(ui.ctx());
ui.close_menu();
}
if ui
.add_enabled(
ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT,
Button::new("Zoom Out")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)),
)
.clicked()
{
zoom_out(ui.ctx());
ui.close_menu();
}
if let Some(native_pixels_per_point) = native_pixels_per_point {
if ui
.add_enabled(
ui.ctx().pixels_per_point() != native_pixels_per_point,
Button::new("Reset Zoom")
.shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)),
)
.clicked()
{
ui.ctx().set_pixels_per_point(native_pixels_per_point);
ui.close_menu();
}
}
}

View file

@ -1,175 +0,0 @@
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
/// egui tracks widgets frame-to-frame using [`Id`]s.
///
/// For instance, if you start dragging a slider one frame, egui stores
/// the sliders [`Id`] as the current active id so that next frame when
/// you move the mouse the same slider changes, even if the mouse has
/// moved outside the slider.
///
/// For some widgets [`Id`]s are also used to persist some state about the
/// widgets, such as Window position or whether not a collapsing header region is open.
///
/// This implies that the [`Id`]s must be unique.
///
/// For simple things like sliders and buttons that don't have any memory and
/// doesn't move we can use the location of the widget as a source of identity.
/// For instance, a slider only needs a unique and persistent ID while you are
/// dragging the slider. As long as it is still while moving, that is fine.
///
/// For things that need to persist state even after moving (windows, collapsing headers)
/// the location of the widgets is obviously not good enough. For instance,
/// a collapsing region needs to remember whether or not it is open even
/// if the layout next frame is different and the collapsing is not lower down
/// on the screen.
///
/// Then there are widgets that need no identifiers at all, like labels,
/// because they have no state nor are interacted with.
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Id(u64);
impl Id {
/// A special [`Id`], in particular as a key to [`crate::Memory::data`]
/// for when there is no particular widget to attach the data.
///
/// The null [`Id`] is still a valid id to use in all circumstances,
/// though obviously it will lead to a lot of collisions if you do use it!
pub fn null() -> Self {
Self(0)
}
pub(crate) fn background() -> Self {
Self(1)
}
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl std::hash::Hash) -> Id {
use std::hash::{BuildHasher, Hasher};
let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
source.hash(&mut hasher);
Id(hasher.finish())
}
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Id {
use std::hash::{BuildHasher, Hasher};
let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0);
child.hash(&mut hasher);
Id(hasher.finish())
}
/// Short and readable summary
pub fn short_debug_format(&self) -> String {
format!("{:04X}", self.0 as u16)
}
#[inline(always)]
pub(crate) fn value(&self) -> u64 {
self.0
}
#[cfg(feature = "accesskit")]
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
std::num::NonZeroU64::new(self.0).unwrap().into()
}
}
impl std::fmt::Debug for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:016X}", self.0)
}
}
/// Convenience
impl From<&'static str> for Id {
#[inline]
fn from(string: &'static str) -> Self {
Self::new(string)
}
}
impl From<String> for Id {
#[inline]
fn from(string: String) -> Self {
Self::new(string)
}
}
// ----------------------------------------------------------------------------
// Idea taken from the `nohash_hasher` crate.
#[derive(Default)]
pub struct IdHasher(u64);
impl std::hash::Hasher for IdHasher {
fn write(&mut self, _: &[u8]) {
unreachable!("Invalid use of IdHasher");
}
fn write_u8(&mut self, _n: u8) {
unreachable!("Invalid use of IdHasher");
}
fn write_u16(&mut self, _n: u16) {
unreachable!("Invalid use of IdHasher");
}
fn write_u32(&mut self, _n: u32) {
unreachable!("Invalid use of IdHasher");
}
#[inline(always)]
fn write_u64(&mut self, n: u64) {
self.0 = n;
}
fn write_usize(&mut self, _n: usize) {
unreachable!("Invalid use of IdHasher");
}
fn write_i8(&mut self, _n: i8) {
unreachable!("Invalid use of IdHasher");
}
fn write_i16(&mut self, _n: i16) {
unreachable!("Invalid use of IdHasher");
}
fn write_i32(&mut self, _n: i32) {
unreachable!("Invalid use of IdHasher");
}
fn write_i64(&mut self, _n: i64) {
unreachable!("Invalid use of IdHasher");
}
fn write_isize(&mut self, _n: isize) {
unreachable!("Invalid use of IdHasher");
}
#[inline(always)]
fn finish(&self) -> u64 {
self.0
}
}
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct BuilIdHasher {}
impl std::hash::BuildHasher for BuilIdHasher {
type Hasher = IdHasher;
#[inline(always)]
fn build_hasher(&self) -> IdHasher {
IdHasher::default()
}
}
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
pub type IdSet = std::collections::HashSet<Id, BuilIdHasher>;
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;

File diff suppressed because it is too large Load diff

View file

@ -1,339 +0,0 @@
use std::{collections::BTreeMap, fmt::Debug};
use crate::{
data::input::TouchDeviceId,
emath::{normalized_angle, Pos2, Vec2},
Event, RawInput, TouchId, TouchPhase,
};
/// All you probably need to know about a multi-touch gesture.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MultiTouchInfo {
/// Point in time when the gesture started.
pub start_time: f64,
/// Position of the pointer at the time the gesture started.
pub start_pos: Pos2,
/// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no
/// [`MultiTouchInfo`] is created.
pub num_touches: usize,
/// Proportional zoom factor (pinch gesture).
/// * `zoom = 1`: no change
/// * `zoom < 1`: pinch together
/// * `zoom > 1`: pinch spread
pub zoom_delta: f32,
/// 2D non-proportional zoom factor (pinch gesture).
///
/// For horizontal pinches, this will return `[z, 1]`,
/// for vertical pinches this will return `[1, z]`,
/// and otherwise this will return `[z, z]`,
/// where `z` is the zoom factor:
/// * `zoom = 1`: no change
/// * `zoom < 1`: pinch together
/// * `zoom > 1`: pinch spread
pub zoom_delta_2d: Vec2,
/// Rotation in radians. Moving fingers around each other will change this value. This is a
/// relative value, comparing the orientation of fingers in the current frame with the previous
/// frame. If all fingers are resting, this value is `0.0`.
pub rotation_delta: f32,
/// Relative movement (comparing previous frame and current frame) of the average position of
/// all touch points. Without movement this value is `Vec2::ZERO`.
///
/// Note that this may not necessarily be measured in screen points (although it _will_ be for
/// most mobile devices). In general (depending on the touch device), touch coordinates cannot
/// be directly mapped to the screen. A touch always is considered to start at the position of
/// the pointer, but touch movement is always measured in the units delivered by the device,
/// and may depend on hardware and system settings.
pub translation_delta: Vec2,
/// Current force of the touch (average of the forces of the individual fingers). This is a
/// value in the interval `[0.0 .. =1.0]`.
///
/// Note 1: A value of `0.0` either indicates a very light touch, or it means that the device
/// is not capable of measuring the touch force at all.
///
/// Note 2: Just increasing the physical pressure without actually moving the finger may not
/// necessarily lead to a change of this value.
pub force: f32,
}
/// The current state (for a specific touch device) of touch events and gestures.
#[derive(Clone)]
pub(crate) struct TouchState {
/// Technical identifier of the touch device. This is used to identify relevant touch events
/// for this [`TouchState`] instance.
device_id: TouchDeviceId,
/// Active touches, if any.
///
/// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The
/// next touch will receive a new unique ID.
///
/// Refer to [`ActiveTouch`].
active_touches: BTreeMap<TouchId, ActiveTouch>,
/// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this
/// holds state information
gesture_state: Option<GestureState>,
}
#[derive(Clone, Debug)]
struct GestureState {
start_time: f64,
start_pointer_pos: Pos2,
pinch_type: PinchType,
previous: Option<DynGestureState>,
current: DynGestureState,
}
/// Gesture data that can change over time
#[derive(Clone, Copy, Debug)]
struct DynGestureState {
/// used for proportional zooming
avg_distance: f32,
/// used for non-proportional zooming
avg_abs_distance2: Vec2,
avg_pos: Pos2,
avg_force: f32,
heading: f32,
}
/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as
/// long as the finger/pen touches the surface.
#[derive(Clone, Copy, Debug)]
struct ActiveTouch {
/// Current position of this touch, in device coordinates (not necessarily screen position)
pos: Pos2,
/// Current force of the touch. A value in the interval [0.0 .. 1.0]
///
/// Note that a value of 0.0 either indicates a very light touch, or it means that the device
/// is not capable of measuring the touch force.
force: f32,
}
impl TouchState {
pub fn new(device_id: TouchDeviceId) -> Self {
Self {
device_id,
active_touches: Default::default(),
gesture_state: None,
}
}
pub fn begin_frame(&mut self, time: f64, new: &RawInput, pointer_pos: Option<Pos2>) {
let mut added_or_removed_touches = false;
for event in &new.events {
match *event {
Event::Touch {
device_id,
id,
phase,
pos,
force,
} if device_id == self.device_id => match phase {
TouchPhase::Start => {
self.active_touches.insert(id, ActiveTouch { pos, force });
added_or_removed_touches = true;
}
TouchPhase::Move => {
if let Some(touch) = self.active_touches.get_mut(&id) {
touch.pos = pos;
touch.force = force;
}
}
TouchPhase::End | TouchPhase::Cancel => {
self.active_touches.remove(&id);
added_or_removed_touches = true;
}
},
_ => (),
}
}
// This needs to be called each frame, even if there are no new touch events.
// Otherwise, we would send the same old delta information multiple times:
self.update_gesture(time, pointer_pos);
if added_or_removed_touches {
// Adding or removing fingers makes the average values "jump". We better forget
// about the previous values, and don't create delta information for this frame:
if let Some(ref mut state) = &mut self.gesture_state {
state.previous = None;
}
}
}
pub fn is_active(&self) -> bool {
self.gesture_state.is_some()
}
pub fn info(&self) -> Option<MultiTouchInfo> {
self.gesture_state.as_ref().map(|state| {
// state.previous can be `None` when the number of simultaneous touches has just
// changed. In this case, we take `current` as `previous`, pretending that there
// was no change for the current frame.
let state_previous = state.previous.unwrap_or(state.current);
let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
let zoom_delta2 = match state.pinch_type {
PinchType::Horizontal => Vec2::new(
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
1.0,
),
PinchType::Vertical => Vec2::new(
1.0,
state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y,
),
PinchType::Proportional => Vec2::splat(zoom_delta),
};
MultiTouchInfo {
start_time: state.start_time,
start_pos: state.start_pointer_pos,
num_touches: self.active_touches.len(),
zoom_delta,
zoom_delta_2d: zoom_delta2,
rotation_delta: normalized_angle(state.current.heading - state_previous.heading),
translation_delta: state.current.avg_pos - state_previous.avg_pos,
force: state.current.avg_force,
}
})
}
fn update_gesture(&mut self, time: f64, pointer_pos: Option<Pos2>) {
if let Some(dyn_state) = self.calc_dynamic_state() {
if let Some(ref mut state) = &mut self.gesture_state {
// updating an ongoing gesture
state.previous = Some(state.current);
state.current = dyn_state;
} else if let Some(pointer_pos) = pointer_pos {
// starting a new gesture
self.gesture_state = Some(GestureState {
start_time: time,
start_pointer_pos: pointer_pos,
pinch_type: PinchType::classify(&self.active_touches),
previous: None,
current: dyn_state,
});
}
} else {
// the end of a gesture (if there is any)
self.gesture_state = None;
}
}
/// `None` if less than two fingers
fn calc_dynamic_state(&self) -> Option<DynGestureState> {
let num_touches = self.active_touches.len();
if num_touches < 2 {
None
} else {
let mut state = DynGestureState {
avg_distance: 0.0,
avg_abs_distance2: Vec2::ZERO,
avg_pos: Pos2::ZERO,
avg_force: 0.0,
heading: 0.0,
};
let num_touches_recip = 1. / num_touches as f32;
// first pass: calculate force and center of touch positions:
for touch in self.active_touches.values() {
state.avg_force += touch.force;
state.avg_pos.x += touch.pos.x;
state.avg_pos.y += touch.pos.y;
}
state.avg_force *= num_touches_recip;
state.avg_pos.x *= num_touches_recip;
state.avg_pos.y *= num_touches_recip;
// second pass: calculate distances from center:
for touch in self.active_touches.values() {
state.avg_distance += state.avg_pos.distance(touch.pos);
state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs();
state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs();
}
state.avg_distance *= num_touches_recip;
state.avg_abs_distance2 *= num_touches_recip;
// Calculate the direction from the first touch to the center position.
// This is not the perfect way of calculating the direction if more than two fingers
// are involved, but as long as all fingers rotate more or less at the same angular
// velocity, the shortcomings of this method will not be noticed. One can see the
// issues though, when touching with three or more fingers, and moving only one of them
// (it takes two hands to do this in a controlled manner). A better technique would be
// to store the current and previous directions (with reference to the center) for each
// touch individually, and then calculate the average of all individual changes in
// direction. But this approach cannot be implemented locally in this method, making
// everything a bit more complicated.
let first_touch = self.active_touches.values().next().unwrap();
state.heading = (state.avg_pos - first_touch.pos).angle();
Some(state)
}
}
}
impl TouchState {
pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(format!("{:?}", self));
}
}
impl Debug for TouchState {
// This outputs less clutter than `#[derive(Debug)]`:
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (id, touch) in &self.active_touches {
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?;
}
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
Ok(())
}
}
#[derive(Clone, Debug)]
enum PinchType {
Horizontal,
Vertical,
Proportional,
}
impl PinchType {
fn classify(touches: &BTreeMap<TouchId, ActiveTouch>) -> Self {
// For non-proportional 2d zooming:
// If the user is pinching with two fingers that have roughly the same Y coord,
// then the Y zoom is unstable and should be 1.
// Similarly, if the fingers are directly above/below each other,
// we should only zoom on the Y axis.
// If the fingers are roughly on a diagonal, we revert to the proportional zooming.
if touches.len() == 2 {
let mut touches = touches.values();
let t0 = touches.next().unwrap().pos;
let t1 = touches.next().unwrap().pos;
let dx = (t0.x - t1.x).abs();
let dy = (t0.y - t1.y).abs();
if dx > 3.0 * dy {
Self::Horizontal
} else if dy > 3.0 * dx {
Self::Vertical
} else {
Self::Proportional
}
} else {
Self::Proportional
}
}
}

View file

@ -1,198 +0,0 @@
//! Showing UI:s for egui/epaint types.
use crate::*;
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
let families = ui.fonts(|f| f.families());
ui.horizontal(|ui| {
for alternative in families {
let text = alternative.to_string();
ui.radio_value(font_family, alternative, text);
}
});
}
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
let families = ui.fonts(|f| f.families());
ui.horizontal(|ui| {
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
for alternative in families {
let text = alternative.to_string();
ui.radio_value(&mut font_id.family, alternative, text);
}
});
}
// Show font texture in demo Ui
pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response {
ui.vertical(|ui| {
let color = if ui.visuals().dark_mode {
Color32::WHITE
} else {
Color32::BLACK
};
ui.label(format!(
"Texture size: {} x {} (hover to zoom)",
width, height
));
if width <= 1 || height <= 1 {
return;
}
let mut size = vec2(width as f32, height as f32);
if size.x > ui.available_width() {
size *= ui.available_width() / size.x;
}
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), color);
ui.painter().add(Shape::mesh(mesh));
let (tex_w, tex_h) = (width as f32, height as f32);
response
.on_hover_cursor(CursorIcon::ZoomIn)
.on_hover_ui_at_pointer(|ui| {
if let Some(pos) = ui.ctx().pointer_latest_pos() {
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);
let texel_radius = 32.0;
let u = u.at_least(texel_radius).at_most(tex_w - texel_radius);
let v = v.at_least(texel_radius).at_most(tex_h - texel_radius);
let uv_rect = Rect::from_min_max(
pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h),
pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h),
);
let mut mesh = Mesh::default();
mesh.add_rect_with_uv(zoom_rect, uv_rect, color);
ui.painter().add(Shape::mesh(mesh));
}
});
})
.response
}
impl Widget for &epaint::stats::PaintStats {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
ui.label(
"egui generates intermediate level shapes like circles and text. \
These are later tessellated into triangles.",
);
ui.add_space(10.0);
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
let epaint::stats::PaintStats {
shapes,
shape_text,
shape_path,
shape_mesh,
shape_vec,
num_callbacks,
text_shape_vertices,
text_shape_indices,
clipped_primitives,
vertices,
indices,
} = self;
ui.label("Intermediate:");
label(ui, shapes, "shapes").on_hover_text("Boxes, circles, etc");
ui.horizontal(|ui| {
label(ui, shape_text, "text");
ui.small("(mostly cached)");
});
label(ui, shape_path, "paths");
label(ui, shape_mesh, "nested meshes");
label(ui, shape_vec, "nested shapes");
ui.label(format!("{:6} callbacks", num_callbacks));
ui.add_space(10.0);
ui.label("Text shapes:");
label(ui, text_shape_vertices, "vertices");
label(ui, text_shape_indices, "indices")
.on_hover_text("Three 32-bit indices per triangles");
ui.add_space(10.0);
ui.label("Tessellated (and culled):");
label(ui, clipped_primitives, "primitives lists")
.on_hover_text("Number of separate clip rectangles");
label(ui, vertices, "vertices");
label(ui, indices, "indices").on_hover_text("Three 32-bit indices per triangles");
ui.add_space(10.0);
// ui.label("Total:");
// ui.label(self.total().format(""));
})
.response
}
}
fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response {
ui.add(Label::new(alloc_info.format(what)).wrap(false))
}
impl Widget for &mut epaint::TessellationOptions {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
let epaint::TessellationOptions {
feathering,
feathering_size_in_pixels,
coarse_tessellation_culling,
prerasterized_discs,
round_text_to_pixels,
debug_paint_clip_rects,
debug_paint_text_rects,
debug_ignore_clip_rects,
bezier_tolerance,
epsilon: _,
} = self;
ui.checkbox(feathering, "Feathering (antialias)")
.on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain.");
let feathering_slider = crate::Slider::new(feathering_size_in_pixels, 0.0..=10.0)
.smallest_positive(0.1)
.logarithmic(true)
.text("Feathering size in pixels");
ui.add_enabled(*feathering, feathering_slider);
ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization");
ui.add(
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
.logarithmic(true)
.show_value(true)
.text("Spline Tolerance"),
);
ui.collapsing("debug", |ui| {
ui.checkbox(
coarse_tessellation_culling,
"Do coarse culling in the tessellator",
);
ui.checkbox(round_text_to_pixels, "Align text positions to pixel grid")
.on_hover_text("Most text already is, so don't expect to see a large change.");
ui.checkbox(debug_ignore_clip_rects, "Ignore clip rectangles");
ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles");
ui.checkbox(debug_paint_text_rects, "Paint text bounds");
});
})
.response
}
}
impl Widget for &memory::Interaction {
fn ui(self, ui: &mut Ui) -> Response {
ui.vertical(|ui| {
ui.label(format!("click_id: {:?}", self.click_id));
ui.label(format!("drag_id: {:?}", self.drag_id));
ui.label(format!("drag_is_window: {:?}", self.drag_is_window));
ui.label(format!("click_interest: {:?}", self.click_interest));
ui.label(format!("drag_interest: {:?}", self.drag_interest));
})
.response
}
}

View file

@ -1,841 +0,0 @@
use crate::{egui_assert, emath::*, Align};
use std::f32::INFINITY;
// ----------------------------------------------------------------------------
/// This describes the bounds and existing contents of an [`Ui`][`crate::Ui`].
/// It is what is used and updated by [`Layout`] when adding new widgets.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Region {
/// This is the minimal size of the [`Ui`](crate::Ui).
/// When adding new widgets, this will generally expand.
///
/// Always finite.
///
/// The bounding box of all child widgets, but not necessarily a tight bounding box
/// since [`Ui`](crate::Ui) can start with a non-zero min_rect size.
pub min_rect: Rect,
/// The maximum size of this [`Ui`](crate::Ui). This is a *soft max*
/// meaning new widgets will *try* not to expand beyond it,
/// but if they have to, they will.
///
/// Text will wrap at `max_rect.right()`.
/// Some widgets (like separator lines) will try to fill the full `max_rect` width of the ui.
///
/// `max_rect` will always be at least the size of `min_rect`.
///
/// If the `max_rect` size is zero, it is a signal that child widgets should be as small as possible.
/// If the `max_rect` size is infinite, it is a signal that child widgets should take up as much room as they want.
pub max_rect: Rect,
/// Where the next widget will be put.
///
/// One side of this will always be infinite: the direction in which new widgets will be added.
/// The opposing side is what is incremented.
/// The crossing sides are initialized to `max_rect`.
///
/// So one can think of `cursor` as a constraint on the available region.
///
/// If something has already been added, this will point to `style.spacing.item_spacing` beyond the latest child.
/// The cursor can thus be `style.spacing.item_spacing` pixels outside of the min_rect.
pub(crate) cursor: Rect,
}
impl Region {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.min_rect = self.min_rect.union(rect);
self.max_rect = self.max_rect.union(rect);
}
/// Ensure we are big enough to contain the given X-coordinate.
/// This is sometimes useful to expand an ui to stretch to a certain place.
pub fn expand_to_include_x(&mut self, x: f32) {
self.min_rect.extend_with_x(x);
self.max_rect.extend_with_x(x);
self.cursor.extend_with_x(x);
}
/// Ensure we are big enough to contain the given Y-coordinate.
/// This is sometimes useful to expand an ui to stretch to a certain place.
pub fn expand_to_include_y(&mut self, y: f32) {
self.min_rect.extend_with_y(y);
self.max_rect.extend_with_y(y);
self.cursor.extend_with_y(y);
}
pub fn sanity_check(&self) {
egui_assert!(!self.min_rect.any_nan());
egui_assert!(!self.max_rect.any_nan());
egui_assert!(!self.cursor.any_nan());
}
}
// ----------------------------------------------------------------------------
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction {
LeftToRight,
RightToLeft,
TopDown,
BottomUp,
}
impl Direction {
#[inline(always)]
pub fn is_horizontal(self) -> bool {
match self {
Direction::LeftToRight | Direction::RightToLeft => true,
Direction::TopDown | Direction::BottomUp => false,
}
}
#[inline(always)]
pub fn is_vertical(self) -> bool {
match self {
Direction::LeftToRight | Direction::RightToLeft => false,
Direction::TopDown | Direction::BottomUp => true,
}
}
}
// ----------------------------------------------------------------------------
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
/// ui.label("world!");
/// ui.label("Hello");
/// });
/// # });
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Layout {
/// Main axis direction
pub main_dir: Direction,
/// If true, wrap around when reading the end of the main direction.
/// For instance, for `main_dir == Direction::LeftToRight` this will
/// wrap to a new row when we reach the right side of the `max_rect`.
pub main_wrap: bool,
/// How to align things on the main axis.
pub main_align: Align,
/// Justify the main axis?
pub main_justify: bool,
/// How to align things on the cross axis.
/// For vertical layouts: put things to left, center or right?
/// For horizontal layouts: put things to top, center or bottom?
pub cross_align: Align,
/// Justify the cross axis?
/// For vertical layouts justify mean all widgets get maximum width.
/// For horizontal layouts justify mean all widgets get maximum height.
pub cross_justify: bool,
}
impl Default for Layout {
fn default() -> Self {
// TODO(emilk): Get from `Style` instead.
Self::top_down(Align::LEFT) // This is a very euro-centric default.
}
}
/// ## Constructors
impl Layout {
/// Place elements horizontally, left to right.
///
/// The `valign` parameter controls how to align elements vertically.
#[inline(always)]
pub fn left_to_right(valign: Align) -> Self {
Self {
main_dir: Direction::LeftToRight,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: valign,
cross_justify: false,
}
}
/// Place elements horizontally, right to left.
///
/// The `valign` parameter controls how to align elements vertically.
#[inline(always)]
pub fn right_to_left(valign: Align) -> Self {
Self {
main_dir: Direction::RightToLeft,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: valign,
cross_justify: false,
}
}
/// Place elements vertically, top to bottom.
///
/// Use the provided horizontal alignment.
#[inline(always)]
pub fn top_down(halign: Align) -> Self {
Self {
main_dir: Direction::TopDown,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: halign,
cross_justify: false,
}
}
/// Top-down layout justified so that buttons etc fill the full available width.
#[inline(always)]
pub fn top_down_justified(halign: Align) -> Self {
Self::top_down(halign).with_cross_justify(true)
}
/// Place elements vertically, bottom up.
///
/// Use the provided horizontal alignment.
#[inline(always)]
pub fn bottom_up(halign: Align) -> Self {
Self {
main_dir: Direction::BottomUp,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: halign,
cross_justify: false,
}
}
#[inline(always)]
pub fn from_main_dir_and_cross_align(main_dir: Direction, cross_align: Align) -> Self {
Self {
main_dir,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align,
cross_justify: false,
}
}
/// For when you want to add a single widget to a layout, and that widget
/// should use up all available space.
///
/// Only one widget may be added to the inner `Ui`!
#[inline(always)]
pub fn centered_and_justified(main_dir: Direction) -> Self {
Self {
main_dir,
main_wrap: false,
main_align: Align::Center,
main_justify: true,
cross_align: Align::Center,
cross_justify: true,
}
}
/// Wrap widgets when we overflow the main axis?
///
/// For instance, for left-to-right layouts, setting this to `true` will
/// put widgets on a new row if we would overflow the right side of [`crate::Ui::max_rect`].
#[inline(always)]
pub fn with_main_wrap(self, main_wrap: bool) -> Self {
Self { main_wrap, ..self }
}
/// The alignment to use on the main axis.
#[inline(always)]
pub fn with_main_align(self, main_align: Align) -> Self {
Self { main_align, ..self }
}
/// The alignment to use on the cross axis.
///
/// The "cross" axis is the one orthogonal to the main axis.
/// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical.
#[inline(always)]
pub fn with_cross_align(self, cross_align: Align) -> Self {
Self {
cross_align,
..self
}
}
/// Justify widgets on the main axis?
///
/// Justify here means "take up all available space".
#[inline(always)]
pub fn with_main_justify(self, main_justify: bool) -> Self {
Self {
main_justify,
..self
}
}
/// Justify widgets along the cross axis?
///
/// Justify here means "take up all available space".
///
/// The "cross" axis is the one orthogonal to the main axis.
/// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical.
#[inline(always)]
pub fn with_cross_justify(self, cross_justify: bool) -> Self {
Self {
cross_justify,
..self
}
}
}
/// ## Inspectors
impl Layout {
#[inline(always)]
pub fn main_dir(&self) -> Direction {
self.main_dir
}
#[inline(always)]
pub fn main_wrap(&self) -> bool {
self.main_wrap
}
#[inline(always)]
pub fn cross_align(&self) -> Align {
self.cross_align
}
#[inline(always)]
pub fn cross_justify(&self) -> bool {
self.cross_justify
}
#[inline(always)]
pub fn is_horizontal(&self) -> bool {
self.main_dir().is_horizontal()
}
#[inline(always)]
pub fn is_vertical(&self) -> bool {
self.main_dir().is_vertical()
}
pub fn prefer_right_to_left(&self) -> bool {
self.main_dir == Direction::RightToLeft
|| self.main_dir.is_vertical() && self.cross_align == Align::Max
}
/// e.g. for adjusting the placement of something.
/// * in horizontal layout: left or right?
/// * in vertical layout: same as [`Self::horizontal_align`].
pub fn horizontal_placement(&self) -> Align {
match self.main_dir {
Direction::LeftToRight => Align::LEFT,
Direction::RightToLeft => Align::RIGHT,
Direction::TopDown | Direction::BottomUp => self.cross_align,
}
}
/// e.g. for when aligning text within a button.
pub fn horizontal_align(&self) -> Align {
if self.is_horizontal() {
self.main_align
} else {
self.cross_align
}
}
/// e.g. for when aligning text within a button.
pub fn vertical_align(&self) -> Align {
if self.is_vertical() {
self.main_align
} else {
self.cross_align
}
}
/// e.g. for when aligning text within a button.
fn align2(&self) -> Align2 {
Align2([self.horizontal_align(), self.vertical_align()])
}
pub fn horizontal_justify(&self) -> bool {
if self.is_horizontal() {
self.main_justify
} else {
self.cross_justify
}
}
pub fn vertical_justify(&self) -> bool {
if self.is_vertical() {
self.main_justify
} else {
self.cross_justify
}
}
}
/// ## Doing layout
impl Layout {
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
egui_assert!(size.x >= 0.0 && size.y >= 0.0);
egui_assert!(!outer.is_negative());
self.align2().align_size_within_rect(size, outer)
}
fn initial_cursor(&self, max_rect: Rect) -> Rect {
let mut cursor = max_rect;
match self.main_dir {
Direction::LeftToRight => {
cursor.max.x = INFINITY;
}
Direction::RightToLeft => {
cursor.min.x = -INFINITY;
}
Direction::TopDown => {
cursor.max.y = INFINITY;
}
Direction::BottomUp => {
cursor.min.y = -INFINITY;
}
}
cursor
}
pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region {
egui_assert!(!max_rect.any_nan());
egui_assert!(max_rect.is_finite());
let mut region = Region {
min_rect: Rect::NOTHING, // temporary
max_rect,
cursor: self.initial_cursor(max_rect),
};
let seed = self.next_widget_position(&region);
region.min_rect = Rect::from_center_size(seed, Vec2::ZERO);
region
}
pub(crate) fn available_rect_before_wrap(&self, region: &Region) -> Rect {
self.available_from_cursor_max_rect(region.cursor, region.max_rect)
}
/// Amount of space available for a widget.
/// For wrapping layouts, this is the maximum (after wrap).
pub(crate) fn available_size(&self, r: &Region) -> Vec2 {
if self.main_wrap {
if self.main_dir.is_horizontal() {
vec2(r.max_rect.width(), r.cursor.height())
} else {
vec2(r.cursor.width(), r.max_rect.height())
}
} else {
self.available_from_cursor_max_rect(r.cursor, r.max_rect)
.size()
}
}
/// Given the cursor in the region, how much space is available
/// for the next widget?
fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect {
egui_assert!(!cursor.any_nan());
egui_assert!(!max_rect.any_nan());
egui_assert!(max_rect.is_finite());
// NOTE: in normal top-down layout the cursor has moved below the current max_rect,
// but the available shouldn't be negative.
// ALSO: with wrapping layouts, cursor jumps to new row before expanding max_rect.
let mut avail = max_rect;
match self.main_dir {
Direction::LeftToRight => {
avail.min.x = cursor.min.x;
avail.max.x = avail.max.x.max(cursor.min.x);
avail.max.x = avail.max.x.max(avail.min.x);
avail.max.y = avail.max.y.max(avail.min.y);
}
Direction::RightToLeft => {
avail.max.x = cursor.max.x;
avail.min.x = avail.min.x.min(cursor.max.x);
avail.min.x = avail.min.x.min(avail.max.x);
avail.max.y = avail.max.y.max(avail.min.y);
}
Direction::TopDown => {
avail.min.y = cursor.min.y;
avail.max.y = avail.max.y.max(cursor.min.y);
avail.max.x = avail.max.x.max(avail.min.x);
avail.max.y = avail.max.y.max(avail.min.y);
}
Direction::BottomUp => {
avail.max.y = cursor.max.y;
avail.min.y = avail.min.y.min(cursor.max.y);
avail.max.x = avail.max.x.max(avail.min.x);
avail.min.y = avail.min.y.min(avail.max.y);
}
}
// We can use the cursor to restrict the available region.
// For instance, we use this to restrict the available space of a parent Ui
// after adding a panel to it.
// We also use it for wrapping layouts.
avail = avail.intersect(cursor);
// Make sure it isn't negative:
if avail.max.x < avail.min.x {
let x = 0.5 * (avail.min.x + avail.max.x);
avail.min.x = x;
avail.max.x = x;
}
if avail.max.y < avail.min.y {
let y = 0.5 * (avail.min.y + avail.max.y);
avail.min.y = y;
avail.max.y = y;
}
egui_assert!(!avail.any_nan());
avail
}
/// Returns where to put the next widget that is of the given size.
/// The returned `frame_rect` [`Rect`] will always be justified along the cross axis.
/// This is what you then pass to `advance_after_rects`.
/// Use `justify_and_align` to get the inner `widget_rect`.
pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect {
region.sanity_check();
egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
if self.main_wrap {
let available_size = self.available_rect_before_wrap(region).size();
let Region {
mut cursor,
mut max_rect,
min_rect,
} = *region;
match self.main_dir {
Direction::LeftToRight => {
if available_size.x < child_size.x && max_rect.left() < cursor.left() {
// New row
let new_row_height = cursor.height().max(child_size.y);
// let new_top = cursor.bottom() + spacing.y;
let new_top = min_rect.bottom() + spacing.y; // tighter packing
cursor = Rect::from_min_max(
pos2(max_rect.left(), new_top),
pos2(INFINITY, new_top + new_row_height),
);
max_rect.max.y = max_rect.max.y.max(cursor.max.y);
}
}
Direction::RightToLeft => {
if available_size.x < child_size.x && cursor.right() < max_rect.right() {
// New row
let new_row_height = cursor.height().max(child_size.y);
// let new_top = cursor.bottom() + spacing.y;
let new_top = min_rect.bottom() + spacing.y; // tighter packing
cursor = Rect::from_min_max(
pos2(-INFINITY, new_top),
pos2(max_rect.right(), new_top + new_row_height),
);
max_rect.max.y = max_rect.max.y.max(cursor.max.y);
}
}
Direction::TopDown => {
if available_size.y < child_size.y && max_rect.top() < cursor.top() {
// New column
let new_col_width = cursor.width().max(child_size.x);
cursor = Rect::from_min_max(
pos2(cursor.right() + spacing.x, max_rect.top()),
pos2(cursor.right() + spacing.x + new_col_width, INFINITY),
);
max_rect.max.x = max_rect.max.x.max(cursor.max.x);
}
}
Direction::BottomUp => {
if available_size.y < child_size.y && cursor.bottom() < max_rect.bottom() {
// New column
let new_col_width = cursor.width().max(child_size.x);
cursor = Rect::from_min_max(
pos2(cursor.right() + spacing.x, -INFINITY),
pos2(
cursor.right() + spacing.x + new_col_width,
max_rect.bottom(),
),
);
max_rect.max.x = max_rect.max.x.max(cursor.max.x);
}
}
}
// Use the new cursor:
let region = Region {
min_rect,
max_rect,
cursor,
};
self.next_frame_ignore_wrap(&region, child_size)
} else {
self.next_frame_ignore_wrap(region, child_size)
}
}
fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect {
region.sanity_check();
egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
let available_rect = self.available_rect_before_wrap(region);
let mut frame_size = child_size;
if (self.is_vertical() && self.horizontal_align() == Align::Center)
|| self.horizontal_justify()
{
frame_size.x = frame_size.x.max(available_rect.width()); // fill full width
}
if (self.is_horizontal() && self.vertical_align() == Align::Center)
|| self.vertical_justify()
{
frame_size.y = frame_size.y.max(available_rect.height()); // fill full height
}
let align2 = match self.main_dir {
Direction::LeftToRight => Align2([Align::LEFT, self.vertical_align()]),
Direction::RightToLeft => Align2([Align::RIGHT, self.vertical_align()]),
Direction::TopDown => Align2([self.horizontal_align(), Align::TOP]),
Direction::BottomUp => Align2([self.horizontal_align(), Align::BOTTOM]),
};
let mut frame_rect = align2.align_size_within_rect(frame_size, available_rect);
if self.is_horizontal() && frame_rect.top() < region.cursor.top() {
// for horizontal layouts we always want to expand down,
// or we will overlap the row above.
// This is a bit hacky. Maybe we should do it for vertical layouts too.
frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top()));
}
egui_assert!(!frame_rect.any_nan());
egui_assert!(!frame_rect.is_negative());
frame_rect
}
/// Apply justify (fill width/height) and/or alignment after calling `next_space`.
pub(crate) fn justify_and_align(&self, frame: Rect, mut child_size: Vec2) -> Rect {
egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0);
egui_assert!(!frame.is_negative());
if self.horizontal_justify() {
child_size.x = child_size.x.at_least(frame.width()); // fill full width
}
if self.vertical_justify() {
child_size.y = child_size.y.at_least(frame.height()); // fill full height
}
self.align_size_within_rect(child_size, frame)
}
pub(crate) fn next_widget_space_ignore_wrap_justify(
&self,
region: &Region,
size: Vec2,
) -> Rect {
let frame = self.next_frame_ignore_wrap(region, size);
let rect = self.align_size_within_rect(size, frame);
egui_assert!(!rect.any_nan());
egui_assert!(!rect.is_negative());
egui_assert!((rect.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY);
egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY);
rect
}
/// Where would the next tiny widget be centered?
pub(crate) fn next_widget_position(&self, region: &Region) -> Pos2 {
self.next_widget_space_ignore_wrap_justify(region, Vec2::ZERO)
.center()
}
/// Advance the cursor by this many points, and allocate in region.
pub(crate) fn advance_cursor(&self, region: &mut Region, amount: f32) {
match self.main_dir {
Direction::LeftToRight => {
region.cursor.min.x += amount;
region.expand_to_include_x(region.cursor.min.x);
}
Direction::RightToLeft => {
region.cursor.max.x -= amount;
region.expand_to_include_x(region.cursor.max.x);
}
Direction::TopDown => {
region.cursor.min.y += amount;
region.expand_to_include_y(region.cursor.min.y);
}
Direction::BottomUp => {
region.cursor.max.y -= amount;
region.expand_to_include_y(region.cursor.max.y);
}
}
}
/// Advance cursor after a widget was added to a specific rectangle.
///
/// * `frame_rect`: the frame inside which a widget was e.g. centered
/// * `widget_rect`: the actual rect used by the widget
pub(crate) fn advance_after_rects(
&self,
cursor: &mut Rect,
frame_rect: Rect,
widget_rect: Rect,
item_spacing: Vec2,
) {
egui_assert!(!cursor.any_nan());
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary
*cursor = cursor.union(frame_rect);
} else {
// this is a new row or column. We temporarily use NAN for what will be filled in later.
match self.main_dir {
Direction::LeftToRight => {
*cursor = Rect::from_min_max(
pos2(f32::NAN, frame_rect.min.y),
pos2(INFINITY, frame_rect.max.y),
);
}
Direction::RightToLeft => {
*cursor = Rect::from_min_max(
pos2(-INFINITY, frame_rect.min.y),
pos2(f32::NAN, frame_rect.max.y),
);
}
Direction::TopDown => {
*cursor = Rect::from_min_max(
pos2(frame_rect.min.x, f32::NAN),
pos2(frame_rect.max.x, INFINITY),
);
}
Direction::BottomUp => {
*cursor = Rect::from_min_max(
pos2(frame_rect.min.x, -INFINITY),
pos2(frame_rect.max.x, f32::NAN),
);
}
};
}
} else {
// Make sure we also expand where we consider adding things (the cursor):
if self.is_horizontal() {
cursor.min.y = cursor.min.y.min(frame_rect.min.y);
cursor.max.y = cursor.max.y.max(frame_rect.max.y);
} else {
cursor.min.x = cursor.min.x.min(frame_rect.min.x);
cursor.max.x = cursor.max.x.max(frame_rect.max.x);
}
}
match self.main_dir {
Direction::LeftToRight => {
cursor.min.x = widget_rect.max.x + item_spacing.x;
}
Direction::RightToLeft => {
cursor.max.x = widget_rect.min.x - item_spacing.x;
}
Direction::TopDown => {
cursor.min.y = widget_rect.max.y + item_spacing.y;
}
Direction::BottomUp => {
cursor.max.y = widget_rect.min.y - item_spacing.y;
}
};
}
/// Move to the next row in a wrapping layout.
/// Otherwise does nothing.
pub(crate) fn end_row(&mut self, region: &mut Region, spacing: Vec2) {
if self.main_wrap {
match self.main_dir {
Direction::LeftToRight => {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(region.max_rect.left(), new_top),
pos2(INFINITY, new_top + region.cursor.height()),
);
}
Direction::RightToLeft => {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(-INFINITY, new_top),
pos2(region.max_rect.right(), new_top + region.cursor.height()),
);
}
Direction::TopDown | Direction::BottomUp => {}
}
}
}
/// Set row height in horizontal wrapping layout.
pub(crate) fn set_row_height(&mut self, region: &mut Region, height: f32) {
if self.main_wrap && self.is_horizontal() {
region.cursor.max.y = region.cursor.min.y + height;
}
}
}
// ----------------------------------------------------------------------------
/// ## Debug stuff
impl Layout {
/// Shows where the next widget is going to be placed
pub(crate) fn paint_text_at_cursor(
&self,
painter: &crate::Painter,
region: &Region,
stroke: epaint::Stroke,
text: impl ToString,
) {
let cursor = region.cursor;
let next_pos = self.next_widget_position(region);
let l = 64.0;
let align = match self.main_dir {
Direction::LeftToRight => {
painter.line_segment([cursor.left_top(), cursor.left_bottom()], stroke);
painter.arrow(next_pos, vec2(l, 0.0), stroke);
Align2([Align::LEFT, self.vertical_align()])
}
Direction::RightToLeft => {
painter.line_segment([cursor.right_top(), cursor.right_bottom()], stroke);
painter.arrow(next_pos, vec2(-l, 0.0), stroke);
Align2([Align::RIGHT, self.vertical_align()])
}
Direction::TopDown => {
painter.line_segment([cursor.left_top(), cursor.right_top()], stroke);
painter.arrow(next_pos, vec2(0.0, l), stroke);
Align2([self.horizontal_align(), Align::TOP])
}
Direction::BottomUp => {
painter.line_segment([cursor.left_bottom(), cursor.right_bottom()], stroke);
painter.arrow(next_pos, vec2(0.0, -l), stroke);
Align2([self.horizontal_align(), Align::BOTTOM])
}
};
painter.debug_text(next_pos, align, stroke.color, text);
}
}

View file

@ -1,573 +0,0 @@
//! `egui`: an easy-to-use GUI in pure Rust!
//!
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.62.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
//!
//! To create a GUI using egui you first need a [`Context`] (by convention referred to by `ctx`).
//! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
//!
//!
//! # Using egui
//!
//! To see what is possible to build with egui you can check out the online demo at <https://www.egui.rs/#demo>.
//!
//! If you like the "learning by doing" approach, clone <https://github.com/emilk/eframe_template> and get started using egui right away.
//!
//! ### A simple example
//!
//! Here is a simple counter that can be incremented and decremented using two buttons:
//! ```
//! fn ui_counter(ui: &mut egui::Ui, counter: &mut i32) {
//! // Put the buttons and label on the same row:
//! ui.horizontal(|ui| {
//! if ui.button("-").clicked() {
//! *counter -= 1;
//! }
//! ui.label(counter.to_string());
//! if ui.button("+").clicked() {
//! *counter += 1;
//! }
//! });
//! }
//! ```
//!
//! In some GUI frameworks this would require defining multiple types and functions with callbacks or message handlers,
//! but thanks to `egui` being immediate mode everything is one self-contained function!
//!
//! ### Getting a [`Ui`]
//!
//! Use one of [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`] to
//! get access to an [`Ui`] where you can put widgets. For example:
//!
//! ```
//! # egui::__run_test_ctx(|ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| {
//! ui.add(egui::Label::new("Hello World!"));
//! ui.label("A shorter and more convenient way to add a label.");
//! if ui.button("Click me").clicked() {
//! // take some action here
//! }
//! });
//! # });
//! ```
//!
//! ### Quick start
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! # let mut my_string = String::new();
//! # let mut my_boolean = true;
//! # let mut my_f32 = 42.0;
//! ui.label("This is a label");
//! ui.hyperlink("https://github.com/emilk/egui");
//! ui.text_edit_singleline(&mut my_string);
//! if ui.button("Click me").clicked() { }
//! ui.add(egui::Slider::new(&mut my_f32, 0.0..=100.0));
//! ui.add(egui::DragValue::new(&mut my_f32));
//!
//! ui.checkbox(&mut my_boolean, "Checkbox");
//!
//! #[derive(PartialEq)]
//! enum Enum { First, Second, Third }
//! # let mut my_enum = Enum::First;
//! ui.horizontal(|ui| {
//! ui.radio_value(&mut my_enum, Enum::First, "First");
//! ui.radio_value(&mut my_enum, Enum::Second, "Second");
//! ui.radio_value(&mut my_enum, Enum::Third, "Third");
//! });
//!
//! ui.separator();
//!
//! # let my_image = egui::TextureId::default();
//! ui.image(my_image, [640.0, 480.0]);
//!
//! ui.collapsing("Click to see what is hidden!", |ui| {
//! ui.label("Not much, as it turns out");
//! });
//! # });
//! ```
//!
//! ## Conventions
//!
//! Conventions unless otherwise specified:
//!
//! * angles are in radians
//! * `Vec2::X` is right and `Vec2::Y` is down.
//! * `Pos2::ZERO` is left top.
//! * Positions and sizes are measured in _points_. Each point may consist of many physical pixels.
//!
//! # Integrating with egui
//!
//! Most likely you are using an existing `egui` backend/integration such as [`eframe`](https://docs.rs/eframe), [`bevy_egui`](https://docs.rs/bevy_egui),
//! or [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad),
//! but if you want to integrate `egui` into a new game engine, this is the section for you.
//!
//! To write your own integration for egui you need to do this:
//!
//! ``` no_run
//! # fn handle_platform_output(_: egui::PlatformOutput) {}
//! # fn gather_input() -> egui::RawInput { egui::RawInput::default() }
//! # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedPrimitive>) {}
//! let mut ctx = egui::Context::default();
//!
//! // Game loop:
//! loop {
//! let raw_input: egui::RawInput = gather_input();
//!
//! let full_output = ctx.run(raw_input, |ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| {
//! ui.label("Hello world!");
//! if ui.button("Click me").clicked() {
//! // take some action here
//! }
//! });
//! });
//! handle_platform_output(full_output.platform_output);
//! let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint
//! paint(full_output.textures_delta, clipped_primitives);
//! }
//! ```
//!
//!
//! # Understanding immediate mode
//!
//! `egui` is an immediate mode GUI library. It is useful to fully grok what "immediate mode" implies.
//!
//! Here is an example to illustrate it:
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! if ui.button("click me").clicked() {
//! take_action()
//! }
//! # });
//! # fn take_action() {}
//! ```
//!
//! This code is being executed each frame at maybe 60 frames per second.
//! Each frame egui does these things:
//!
//! * lays out the letters `click me` in order to figure out the size of the button
//! * decides where on screen to place the button
//! * check if the mouse is hovering or clicking that location
//! * chose button colors based on if it is being hovered or clicked
//! * add a [`Shape::Rect`] and [`Shape::Text`] to the list of shapes to be painted later this frame
//! * return a [`Response`] with the [`clicked`](`Response::clicked`) member so the user can check for interactions
//!
//! There is no button being created and stored somewhere.
//! The only output of this call is some colored shapes, and a [`Response`].
//!
//! Similarly, consider this code:
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! # let mut value: f32 = 0.0;
//! ui.add(egui::Slider::new(&mut value, 0.0..=100.0).text("My value"));
//! # });
//! ```
//!
//! Here egui will read `value` to display the slider, then look if the mouse is dragging the slider and if so change the `value`.
//! Note that `egui` does not store the slider value for you - it only displays the current value, and changes it
//! by how much the slider has been dragged in the previous few milliseconds.
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
//!
//! It can be useful to read the code for the toggle switch example widget to get a better understanding
//! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//!
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
//!
//! # Misc
//!
//! ## How widgets works
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! if ui.button("click me").clicked() { take_action() }
//! # });
//! # fn take_action() {}
//! ```
//!
//! is short for
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! let button = egui::Button::new("click me");
//! if ui.add(button).clicked() { take_action() }
//! # });
//! # fn take_action() {}
//! ```
//!
//! which is short for
//!
//! ```
//! # use egui::Widget;
//! # egui::__run_test_ui(|ui| {
//! let button = egui::Button::new("click me");
//! let response = button.ui(ui);
//! if response.clicked() { take_action() }
//! # });
//! # fn take_action() {}
//! ```
//!
//! [`Button`] uses the builder pattern to create the data required to show it. The [`Button`] is then discarded.
//!
//! [`Button`] implements `trait` [`Widget`], which looks like this:
//! ```
//! # use egui::*;
//! pub trait Widget {
//! /// Allocate space, interact, paint, and return a [`Response`].
//! fn ui(self, ui: &mut Ui) -> Response;
//! }
//! ```
//!
//! ## Auto-sizing panels and windows
//! In egui, all panels and windows auto-shrink to fit the content.
//! If the window or panel is also resizable, this can lead to a weird behavior
//! where you can drag the edge of the panel/window to make it larger, and
//! when you release the panel/window shrinks again.
//! This is an artifact of immediate mode, and here are some alternatives on how to avoid it:
//!
//! 1. Turn off resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`].
//! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`].
//! 3. Use a justified layout:
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! ui.with_layout(egui::Layout::top_down_justified(egui::Align::Center), |ui| {
//! ui.button("I am becoming wider as needed");
//! });
//! # });
//! ```
//!
//! 4. Fill in extra space with emptiness:
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! ui.allocate_space(ui.available_size()); // put this LAST in your panel/window code
//! # });
//! ```
//!
//! ## Sizes
//! You can control the size of widgets using [`Ui::add_sized`].
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! # let mut my_value = 0.0_f32;
//! ui.add_sized([40.0, 20.0], egui::DragValue::new(&mut my_value));
//! # });
//! ```
//!
//! ## Code snippets
//!
//! ```
//! # egui::__run_test_ui(|ui| {
//! # let mut some_bool = true;
//! // Miscellaneous tips and tricks
//!
//! ui.horizontal_wrapped(|ui| {
//! ui.spacing_mut().item_spacing.x = 0.0; // remove spacing between widgets
//! // `radio_value` also works for enums, integers, and more.
//! ui.radio_value(&mut some_bool, false, "Off");
//! ui.radio_value(&mut some_bool, true, "On");
//! });
//!
//! ui.group(|ui| {
//! ui.label("Within a frame");
//! ui.set_min_height(200.0);
//! });
//!
//! // A `scope` creates a temporary [`Ui`] in which you can change settings:
//! ui.scope(|ui| {
//! ui.visuals_mut().override_text_color = Some(egui::Color32::RED);
//! ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
//! ui.style_mut().wrap = Some(false);
//!
//! ui.label("This text will be red, monospace, and won't wrap to a new line");
//! }); // the temporary settings are reverted here
//! # });
//! ```
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
mod animation_manager;
pub mod containers;
mod context;
mod data;
mod frame_state;
pub(crate) mod grid;
pub mod gui_zoom;
mod id;
mod input_state;
pub mod introspection;
pub mod layers;
mod layout;
mod memory;
pub mod menu;
pub mod os;
mod painter;
pub(crate) mod placer;
mod response;
mod sense;
pub mod style;
mod ui;
pub mod util;
pub mod widget_text;
pub mod widgets;
#[cfg(feature = "accesskit")]
pub use accesskit;
pub use epaint;
pub use epaint::ecolor;
pub use epaint::emath;
#[cfg(feature = "color-hex")]
pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
pub use epaint::{
mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TexturesDelta},
ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo,
Rounding, Shape, Stroke, TextureHandle, TextureId,
};
pub mod text {
pub use crate::text_edit::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TAB_SIZE,
};
}
pub use {
containers::*,
context::Context,
data::{
input::*,
output::{self, CursorIcon, FullOutput, PlatformOutput, WidgetInfo},
},
grid::Grid,
id::{Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order},
layout::*,
memory::{Memory, Options},
painter::Painter,
response::{InnerResponse, Response},
sense::Sense,
style::{FontSelection, Margin, Style, TextStyle, Visuals},
text::{Galley, TextFormat},
ui::Ui,
widget_text::{RichText, WidgetText},
widgets::*,
};
// ----------------------------------------------------------------------------
/// Helper function that adds a label when compiling with debug assertions enabled.
pub fn warn_if_debug_build(ui: &mut crate::Ui) {
if cfg!(debug_assertions) {
ui.label(
RichText::new("⚠ Debug build ⚠")
.small()
.color(ui.visuals().warn_fg_color),
)
.on_hover_text("egui was compiled with debug assertions enabled.");
}
}
// ----------------------------------------------------------------------------
/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// # });
/// ```
#[macro_export]
macro_rules! github_link_file_line {
($github_url: expr, $label: expr) => {{
let url = format!("{}{}#L{}", $github_url, file!(), line!());
$crate::Hyperlink::from_label_and_url($label, url)
}};
}
/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] on github.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// # });
/// ```
#[macro_export]
macro_rules! github_link_file {
($github_url: expr, $label: expr) => {{
let url = format!("{}{}", $github_url, file!());
$crate::Hyperlink::from_label_and_url($label, url)
}};
}
// ----------------------------------------------------------------------------
/// Show debug info on hover when [`Context::set_debug_on_hover`] has been turned on.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// // Turn on tracing of widgets
/// ui.ctx().set_debug_on_hover(true);
///
/// /// Show [`std::file`], [`std::line`] and argument on hover
/// egui::trace!(ui, "MyWindow");
///
/// /// Show [`std::file`] and [`std::line`] on hover
/// egui::trace!(ui);
/// # });
/// ```
#[macro_export]
macro_rules! trace {
($ui: expr) => {{
$ui.trace_location(format!("{}:{}", file!(), line!()))
}};
($ui: expr, $label: expr) => {{
$ui.trace_location(format!("{} - {}:{}", $label, file!(), line!()))
}};
}
// ----------------------------------------------------------------------------
/// An assert that is only active when `egui` is compiled with the `extra_asserts` feature
/// or with the `extra_debug_asserts` feature in debug builds.
#[macro_export]
macro_rules! egui_assert {
($($arg: tt)*) => {
if cfg!(any(
feature = "extra_asserts",
all(feature = "extra_debug_asserts", debug_assertions),
)) {
assert!($($arg)*);
}
}
}
// ----------------------------------------------------------------------------
/// The default egui fonts supports around 1216 emojis in total.
/// Here are some of the most useful:
/// ∞⊗⎗⎘⎙⏏⏴⏵⏶⏷
/// ⏩⏪⏭⏮⏸⏹⏺■▶📾🔀🔁🔃
/// ☀☁★☆☐☑☜☝☞☟⛃⛶✔
/// ↺↻⟲⟳⬅➡⬆⬇⬈⬉⬊⬋⬌⬍⮨⮩⮪⮫
/// ♡
/// 📅📆
/// 📈📉📊
/// 📋📌📎📤📥🔆
/// 🔈🔉🔊🔍🔎🔗🔘
/// 🕓🖧🖩🖮🖱🖴🖵🖼🗀🗁🗋🗐🗑🗙🚫❓
///
/// NOTE: In egui all emojis are monochrome!
///
/// You can explore them all in the Font Book in [the online demo](https://www.egui.rs/#demo).
///
/// In addition, egui supports a few special emojis that are not part of the unicode standard.
/// This module contains some of them:
pub mod special_emojis {
/// Tux, the Linux penguin.
pub const OS_LINUX: char = '🐧';
/// The Windows logo.
pub const OS_WINDOWS: char = '';
/// The Android logo.
pub const OS_ANDROID: char = '';
/// The Apple logo.
pub const OS_APPLE: char = '';
/// The Github logo.
pub const GITHUB: char = '';
/// The Twitter bird.
pub const TWITTER: char = '';
/// The word `git`.
pub const GIT: char = '';
// I really would like to have ferris here.
}
/// The different types of built-in widgets in egui
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WidgetType {
Label, // TODO(emilk): emit Label events
/// e.g. a hyperlink
Link,
TextEdit,
Button,
Checkbox,
RadioButton,
SelectableLabel,
ComboBox,
Slider,
DragValue,
ColorButton,
ImageButton,
CollapsingHeader,
/// If you cannot fit any of the above slots.
///
/// If this is something you think should be added, file an issue.
Other,
}
// ----------------------------------------------------------------------------
/// For use in tests; especially doctests.
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run(Default::default(), |ctx| {
run_ui(ctx);
});
}
/// For use in tests; especially doctests.
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run(Default::default(), |ctx| {
crate::CentralPanel::default().show(ctx, |ui| {
add_contents(ui);
});
});
}
#[cfg(feature = "accesskit")]
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}

View file

@ -1,647 +0,0 @@
use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
// ----------------------------------------------------------------------------
/// The data that egui persists between frames.
///
/// This includes window positions and sizes,
/// how far the user has scrolled in a [`ScrollArea`](crate::ScrollArea) etc.
///
/// If you want this to persist when closing your app you should serialize [`Memory`] and store it.
/// For this you need to enable the `persistence`.
///
/// If you want to store data for your widgets, you should look at [`Memory::data`]
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct Memory {
pub options: Options,
/// This map stores some superficial state for all widgets with custom [`Id`]s.
///
/// This includes storing if a [`crate::CollapsingHeader`] is open, how far scrolled a
/// [`crate::ScrollArea`] is, where the cursor in a [`crate::TextEdit`] is, etc.
///
/// This is NOT meant to store any important data. Store that in your own structures!
///
/// Each read clones the data, so keep your values cheap to clone.
/// If you want to store a lot of data you should wrap it in `Arc<Mutex<…>>` so it is cheap to clone.
///
/// This will be saved between different program runs if you use the `persistence` feature.
///
/// To store a state common for all your widgets (a singleton), use [`Id::null`] as the key.
pub data: crate::util::IdTypeMap,
// ------------------------------------------
/// Can be used to cache computations from one frame to another.
///
/// This is for saving CPU when you have something that may take 1-100ms to compute.
/// Things that are very slow (>100ms) should instead be done async (i.e. in another thread)
/// so as not to lock the UI thread.
///
/// ```
/// use egui::util::cache::{ComputerMut, FrameCache};
///
/// #[derive(Default)]
/// struct CharCounter {}
/// impl ComputerMut<&str, usize> for CharCounter {
/// fn compute(&mut self, s: &str) -> usize {
/// s.chars().count() // you probably want to cache something more expensive than this
/// }
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut ctx = egui::Context::default();
/// ctx.memory_mut(|mem| {
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// });
/// ```
#[cfg_attr(feature = "persistence", serde(skip))]
pub caches: crate::util::cache::CacheStorage,
// ------------------------------------------
/// new scale that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_pixels_per_point: Option<f32>,
/// new fonts that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_font_definitions: Option<epaint::text::FontDefinitions>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) interaction: Interaction,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) window_interaction: Option<window::WindowInteraction>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) drag_value: crate::widgets::drag_value::MonoState,
pub(crate) areas: Areas,
/// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu etc.
#[cfg_attr(feature = "persistence", serde(skip))]
popup: Option<Id>,
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
}
// ----------------------------------------------------------------------------
/// Some global options that you can read and write.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Options {
/// The default style for new [`Ui`](crate::Ui):s.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) style: std::sync::Arc<Style>,
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
///
/// The only change to egui is that labels can be focused by pressing tab.
///
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
///
/// `eframe` supports it only on web, using the `web_screen_reader` feature flag,
/// but you should consider using [AccessKit](https://github.com/AccessKit/accesskit) instead,
/// which `eframe` supports.
pub screen_reader: bool,
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
///
/// Only the fonts in [`Style::text_styles`] will be pre-cached.
///
/// This can lead to fewer texture operations, but may use up the texture atlas quicker
/// if you are changing [`Style::text_styles`], of have a lot of text styles.
pub preload_font_glyphs: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
style: Default::default(),
tessellation_options: Default::default(),
screen_reader: false,
preload_font_glyphs: true,
}
}
}
// ----------------------------------------------------------------------------
/// Say there is a button in a scroll area.
/// If the user clicks the button, the button should click.
/// If the user drags the button we should scroll the scroll area.
/// So what we do is that when the mouse is pressed we register both the button
/// and the scroll area (as `click_id`/`drag_id`).
/// If the user releases the button without moving the mouse we register it as a click on `click_id`.
/// If the cursor moves too much we clear the `click_id` and start passing move events to `drag_id`.
#[derive(Clone, Debug, Default)]
pub(crate) struct Interaction {
/// A widget interested in clicks that has a mouse press on it.
pub click_id: Option<Id>,
/// A widget interested in drags that has a mouse press on it.
pub drag_id: Option<Id>,
pub focus: Focus,
/// HACK: windows have low priority on dragging.
/// This is so that if you drag a slider in a window,
/// the slider will steal the drag away from the window.
/// This is needed because we do window interaction first (to prevent frame delay),
/// and then do content layout.
pub drag_is_window: bool,
/// Any interest in catching clicks this frame?
/// Cleared to false at start of each frame.
pub click_interest: bool,
/// Any interest in catching clicks this frame?
/// Cleared to false at start of each frame.
pub drag_interest: bool,
}
/// Keeps tracks of what widget has keyboard focus
#[derive(Clone, Debug, Default)]
pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field).
pub(crate) id: Option<Id>,
/// What had keyboard focus previous frame?
id_previous_frame: Option<Id>,
/// Give focus to this widget next frame
id_next_frame: Option<Id>,
#[cfg(feature = "accesskit")]
id_requested_by_accesskit: Option<accesskit::NodeId>,
/// If set, the next widget that is interested in focus will automatically get it.
/// Probably because the user pressed Tab.
give_to_next: bool,
/// The last widget interested in focus.
last_interested: Option<Id>,
/// If `true`, pressing tab will NOT move focus away from the current widget.
is_focus_locked: bool,
/// Set at the beginning of the frame, set to `false` when "used".
pressed_tab: bool,
/// Set at the beginning of the frame, set to `false` when "used".
pressed_shift_tab: bool,
}
impl Interaction {
/// Are we currently clicking or dragging an egui widget?
pub fn is_using_pointer(&self) -> bool {
self.click_id.is_some() || self.drag_id.is_some()
}
fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
) {
self.click_interest = false;
self.drag_interest = false;
if !prev_input.pointer.could_any_button_be_click() {
self.click_id = None;
}
if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
// pointer button was not down last frame
self.click_id = None;
self.drag_id = None;
}
self.focus.begin_frame(new_input);
}
}
impl Focus {
/// Which widget currently has keyboard focus?
pub fn focused(&self) -> Option<Id> {
self.id
}
fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
self.id_previous_frame = self.id;
if let Some(id) = self.id_next_frame.take() {
self.id = Some(id);
}
#[cfg(feature = "accesskit")]
{
self.id_requested_by_accesskit = None;
}
self.pressed_tab = false;
self.pressed_shift_tab = false;
for event in &new_input.events {
if matches!(
event,
crate::Event::Key {
key: crate::Key::Escape,
pressed: true,
modifiers: _,
..
}
) {
self.id = None;
self.is_focus_locked = false;
break;
}
if let crate::Event::Key {
key: crate::Key::Tab,
pressed: true,
modifiers,
..
} = event
{
if !self.is_focus_locked {
if modifiers.shift {
self.pressed_shift_tab = true;
} else {
self.pressed_tab = true;
}
}
}
#[cfg(feature = "accesskit")]
{
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus,
target,
data: None,
}) = event
{
self.id_requested_by_accesskit = Some(*target);
}
}
}
}
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
if let Some(id) = self.id {
// Allow calling `request_focus` one frame and not using it until next frame
let recently_gained_focus = self.id_previous_frame != Some(id);
if !recently_gained_focus && !used_ids.contains_key(&id) {
// Dead-mans-switch: the widget with focus has disappeared!
self.id = None;
}
}
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.id_previous_frame == Some(id)
}
fn interested_in_focus(&mut self, id: Id) {
#[cfg(feature = "accesskit")]
{
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
self.id = Some(id);
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.pressed_tab = false;
self.pressed_shift_tab = false;
}
}
if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id);
self.give_to_next = false;
} else if self.id == Some(id) {
if self.pressed_tab && !self.is_focus_locked {
self.id = None;
self.give_to_next = true;
self.pressed_tab = false;
} else if self.pressed_shift_tab && !self.is_focus_locked {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.pressed_shift_tab = false;
}
} else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
self.id = Some(id);
self.pressed_tab = false;
}
self.last_interested = Some(id);
}
}
impl Memory {
pub(crate) fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
) {
self.interaction.begin_frame(prev_input, new_input);
if !prev_input.pointer.any_down() {
self.window_interaction = None;
}
}
pub(crate) fn end_frame(&mut self, input: &InputState, used_ids: &IdMap<Rect>) {
self.caches.update();
self.areas.end_frame();
self.interaction.focus.end_frame(used_ids);
self.drag_value.end_frame(input);
}
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
self.areas.layer_id_at(pos, resize_interact_radius_side)
}
/// An iterator over all layers. Back-to-front. Top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas.order().iter().copied()
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.interaction.focus.id_previous_frame == Some(id)
}
/// True if the given widget had keyboard focus last frame, but not this one.
pub(crate) fn lost_focus(&self, id: Id) -> bool {
self.had_focus_last_frame(id) && !self.has_focus(id)
}
/// True if the given widget has keyboard focus this frame, but didn't last frame.
pub(crate) fn gained_focus(&self, id: Id) -> bool {
!self.had_focus_last_frame(id) && self.has_focus(id)
}
/// Does this widget have keyboard focus?
///
/// This function does not consider whether the UI as a whole (e.g. window)
/// has the keyboard focus. That makes this function suitable for deciding
/// widget state that should not be disrupted if the user moves away
/// from the window and back.
#[inline(always)]
pub fn has_focus(&self, id: Id) -> bool {
self.interaction.focus.id == Some(id)
}
/// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> {
self.interaction.focus.id
}
/// Prevent keyboard focus from moving away from this widget even if users presses the tab key.
/// You must first give focus to the widget before calling this.
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked = lock_focus;
}
}
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {
false
}
}
/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.id = Some(id);
self.interaction.focus.is_focus_locked = false;
}
/// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`].
#[inline(always)]
pub fn surrender_focus(&mut self, id: Id) {
if self.interaction.focus.id == Some(id) {
self.interaction.focus.id = None;
self.interaction.focus.is_focus_locked = false;
}
}
/// Register this widget as being interested in getting keyboard focus.
/// This will allow the user to select it with tab and shift-tab.
/// This is normally done automatically when handling interactions,
/// but it is sometimes useful to pre-register interest in focus,
/// e.g. before deciding which type of underlying widget to use,
/// as in the [`crate::DragValue`] widget, so a widget can be focused
/// and rendered correctly in a single frame.
#[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) {
self.interaction.focus.interested_in_focus(id);
}
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
self.interaction.focus.id = None;
}
#[inline(always)]
pub fn is_anything_being_dragged(&self) -> bool {
self.interaction.drag_id.is_some()
}
#[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction.drag_id == Some(id)
}
#[inline(always)]
pub fn set_dragged_id(&mut self, id: Id) {
self.interaction.drag_id = Some(id);
}
/// Forget window positions, sizes etc.
/// Can be used to auto-layout windows.
pub fn reset_areas(&mut self) {
self.areas = Default::default();
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be be open at a time.
impl Memory {
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popup == Some(popup_id) || self.everything_is_visible()
}
pub fn any_popup_open(&self) -> bool {
self.popup.is_some() || self.everything_is_visible()
}
pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some(popup_id);
}
pub fn close_popup(&mut self) {
self.popup = None;
}
pub fn toggle_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.close_popup();
} else {
self.open_popup(popup_id);
}
}
/// If true, all windows, menus, tooltips etc are to be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
///
/// Experimental feature!
#[inline(always)]
pub fn everything_is_visible(&self) -> bool {
self.everything_is_visible
}
/// If true, all windows, menus, tooltips etc are to be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
///
/// Experimental feature!
pub fn set_everything_is_visible(&mut self, value: bool) {
self.everything_is_visible = value;
}
}
// ----------------------------------------------------------------------------
/// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s.
/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`](crate::Order).
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Areas {
areas: IdMap<area::State>,
/// Back-to-front. Top is last.
order: Vec<LayerId>,
visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,
/// When an area want to be on top, it is put in here.
/// At the end of the frame, this is used to reorder the layers.
/// This means if several layers want to be on top, they will keep their relative order.
/// So if you close three windows and then reopen them all in one frame,
/// they will all be sent to the top, but keep their previous internal order.
wants_to_be_on_top: ahash::HashSet<LayerId>,
}
impl Areas {
pub(crate) fn count(&self) -> usize {
self.areas.len()
}
pub(crate) fn get(&self, id: Id) -> Option<&area::State> {
self.areas.get(&id)
}
/// Back-to-front. Top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
}
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::State) {
self.visible_current_frame.insert(layer_id);
self.areas.insert(layer_id.id, state);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
}
}
/// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
for layer in self.order.iter().rev() {
if self.is_visible(layer) {
if let Some(state) = self.areas.get(&layer.id) {
let mut rect = state.rect();
if state.interactable {
// Allow us to resize by dragging just outside the window:
rect = rect.expand(resize_interact_radius_side);
if rect.contains(pos) {
return Some(*layer);
}
}
}
}
}
None
}
pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id)
}
pub fn is_visible(&self, layer_id: &LayerId) -> bool {
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame
.iter()
.copied()
.chain(self.visible_current_frame.iter().copied())
.collect()
}
pub(crate) fn visible_windows(&self) -> Vec<&area::State> {
self.visible_layer_ids()
.iter()
.filter(|layer| layer.order == crate::Order::Middle)
.filter_map(|layer| self.get(layer.id))
.collect()
}
pub fn move_to_top(&mut self, layer_id: LayerId) {
self.visible_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
self.order.push(layer_id);
}
}
pub(crate) fn end_frame(&mut self) {
let Self {
visible_last_frame,
visible_current_frame,
order,
wants_to_be_on_top,
..
} = self;
std::mem::swap(visible_last_frame, visible_current_frame);
visible_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
wants_to_be_on_top.clear();
}
}
// ----------------------------------------------------------------------------
#[test]
fn memory_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Memory>();
}

View file

@ -1,690 +0,0 @@
//! Menu bar functionality (very basic so far).
//!
//! Usage:
//! ```
//! fn show_menu(ui: &mut egui::Ui) {
//! use egui::{menu, Button};
//!
//! menu::bar(ui, |ui| {
//! ui.menu_button("File", |ui| {
//! if ui.button("Open").clicked() {
//! // …
//! }
//! });
//! });
//! }
//! ```
use super::{
style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
Sense, TextStyle, Ui, Vec2,
};
use crate::{widgets::*, *};
use epaint::mutex::RwLock;
use std::sync::Arc;
/// What is saved between frames.
#[derive(Clone, Default)]
pub(crate) struct BarState {
open_menu: MenuRootManager,
}
impl BarState {
fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
/// Show a menu at pointer if primary-clicked response.
/// Should be called from [`Context`] on a [`Response`]
pub fn bar_menu<R>(
&mut self,
response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
MenuRoot::stationary_click_interaction(response, &mut self.open_menu, response.id);
self.open_menu.show(response, add_contents)
}
}
impl std::ops::Deref for BarState {
type Target = MenuRootManager;
fn deref(&self) -> &Self::Target {
&self.open_menu
}
}
impl std::ops::DerefMut for BarState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.open_menu
}
}
fn set_menu_style(style: &mut Style) {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
/// The menu bar goes well in a [`TopBottomPanel::top`],
/// but can also be placed in a [`Window`].
/// In the latter case you may want to wrap it in [`Frame`].
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());
// Take full width and fixed height:
let height = ui.spacing().interact_size.y;
ui.set_min_size(vec2(ui.available_width(), height));
add_contents(ui)
})
}
/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_button<R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents))
}
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_image_button<R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
}
/// Construct a nested sub menu in another menu.
///
/// Opens on hover.
///
/// Returns `None` if the menu is not open.
pub(crate) fn submenu_button<R>(
ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents)
}
/// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>(
ctx: &Context,
menu_id: impl Into<Id>,
menu_state_arc: &Arc<RwLock<MenuState>>,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
let pos = {
let mut menu_state = menu_state_arc.write();
menu_state.entry_count = 0;
menu_state.rect.min
};
let area = Area::new(menu_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.interactable(true)
.drag_bounds(ctx.screen_rect());
let inner_response = area.show(ctx, |ui| {
set_menu_style(ui.style_mut());
Frame::menu(ui.style())
.show(ui, |ui| {
const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO(emilk): add to ui.spacing
ui.set_max_width(DEFAULT_MENU_WIDTH);
ui.set_menu_state(Some(menu_state_arc.clone()));
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
.inner
})
.inner
});
menu_state_arc.write().rect = inner_response.response.rect;
inner_response
}
/// Build a top level menu with a button.
///
/// Responds to primary clicks.
fn stationary_menu_impl<'c, R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let title = title.into();
let bar_id = ui.id();
let menu_id = bar_id.with(title.text());
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Build a top level menu with an image button.
///
/// Responds to primary clicks.
fn stationary_menu_image_impl<'c, R>(
ui: &mut Ui,
texture_id: TextureId,
image_size: impl Into<Vec2>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(ImageButton::new(texture_id, image_size));
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub(crate) fn context_menu(
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
let menu_id = Id::new("__egui::context_menu");
let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state, response.id);
let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id);
inner_response
}
/// Stores the state for the context menu.
#[derive(Clone, Default)]
pub(crate) struct MenuRootManager {
inner: Option<MenuRoot>,
}
impl MenuRootManager {
/// Show a menu at pointer if right-clicked response.
/// Should be called from [`Context`] on a [`Response`]
pub fn show<R>(
&mut self,
response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
if let Some(root) = self.inner.as_mut() {
let (menu_response, inner_response) = root.show(response, add_contents);
if MenuResponse::Close == menu_response {
self.inner = None;
}
inner_response
} else {
None
}
}
fn is_menu_open(&self, id: Id) -> bool {
self.inner.as_ref().map(|m| m.id) == Some(id)
}
}
impl std::ops::Deref for MenuRootManager {
type Target = Option<MenuRoot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for MenuRootManager {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Menu root associated with an Id from a Response
#[derive(Clone)]
pub(crate) struct MenuRoot {
pub menu_state: Arc<RwLock<MenuState>>,
pub id: Id,
}
impl MenuRoot {
pub fn new(position: Pos2, id: Id) -> Self {
Self {
menu_state: Arc::new(RwLock::new(MenuState::new(position))),
id,
}
}
pub fn show<R>(
&mut self,
response: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (MenuResponse, Option<InnerResponse<R>>) {
if self.id == response.id {
let inner_response =
MenuState::show(&response.ctx, &self.menu_state, self.id, add_contents);
let mut menu_state = self.menu_state.write();
menu_state.rect = inner_response.response.rect;
if menu_state.response.is_close() {
return (MenuResponse::Close, Some(inner_response));
}
}
(MenuResponse::Stay, None)
}
/// Interaction with a stationary menu, i.e. fixed in another Ui.
///
/// Responds to primary clicks.
fn stationary_interaction(
response: &Response,
root: &mut MenuRootManager,
id: Id,
) -> MenuResponse {
if (response.clicked() && root.is_menu_open(id))
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
{
// menu open and button clicked or esc pressed
return MenuResponse::Close;
} else if (response.clicked() && !root.is_menu_open(id))
|| (response.hovered() && root.is_some())
{
// menu not open and button clicked
// or button hovered while other menu is open
let mut pos = response.rect.left_bottom();
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let screen_rect = response.ctx.input(|i| i.screen_rect);
if pos.y + menu_rect.height() > screen_rect.max.y {
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
}
if pos.x + menu_rect.width() > screen_rect.max.x {
pos.x = screen_rect.max.x - menu_rect.width();
}
}
return MenuResponse::Create(pos, id);
} else if response
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
{
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
if let Some(root) = root.inner.as_mut() {
if root.id == id {
// pressed somewhere while this menu is open
let menu_state = root.menu_state.read();
let in_menu = menu_state.area_contains(pos);
if !in_menu {
return MenuResponse::Close;
}
}
}
}
}
MenuResponse::Stay
}
/// Interaction with a context menu (secondary clicks).
fn context_interaction(
response: &Response,
root: &mut Option<MenuRoot>,
id: Id,
) -> MenuResponse {
let response = response.interact(Sense::click());
response.ctx.input(|input| {
let pointer = &input.pointer;
if pointer.any_pressed() {
if let Some(pos) = pointer.interact_pos() {
let mut destroy = false;
let mut in_old_menu = false;
if let Some(root) = root {
let menu_state = root.menu_state.read();
in_old_menu = menu_state.area_contains(pos);
destroy = root.id == response.id;
}
if !in_old_menu {
if response.hovered() && pointer.secondary_down() {
return MenuResponse::Create(pos, id);
} else if (response.hovered() && pointer.primary_down()) || destroy {
return MenuResponse::Close;
}
}
}
}
MenuResponse::Stay
})
}
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
match menu_response {
MenuResponse::Create(pos, id) => {
root.inner = Some(MenuRoot::new(pos, id));
}
MenuResponse::Close => root.inner = None,
MenuResponse::Stay => {}
}
}
/// Respond to secondary (right) clicks.
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) {
let menu_response = Self::context_interaction(response, root, id);
Self::handle_menu_response(root, menu_response);
}
// Responds to primary clicks.
pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) {
let menu_response = Self::stationary_interaction(response, root, id);
Self::handle_menu_response(root, menu_response);
}
}
#[derive(Copy, Clone, PartialEq)]
pub(crate) enum MenuResponse {
Close,
Stay,
Create(Pos2, Id),
}
impl MenuResponse {
pub fn is_close(&self) -> bool {
*self == Self::Close
}
}
pub struct SubMenuButton {
text: WidgetText,
icon: WidgetText,
index: usize,
}
impl SubMenuButton {
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
Self {
text: text.into(),
icon: icon.into(),
index,
}
}
fn visuals<'a>(
ui: &'a Ui,
response: &'_ Response,
menu_state: &'_ MenuState,
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) {
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
}
pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.into();
self
}
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let SubMenuButton { text, icon, .. } = self;
let text_style = TextStyle::Button;
let sense = Sense::click();
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley =
text.into_galley(ui, Some(true), text_available_width, text_style.clone());
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style);
let text_and_icon_size = Vec2::new(
text_galley.size().x + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
crate::WidgetInfo::labeled(crate::WidgetType::Button, text_galley.text())
});
if ui.is_rect_visible(rect) {
let visuals = Self::visuals(ui, &response, menu_state, sub_id);
let text_pos = Align2::LEFT_CENTER
.align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
.min;
let icon_pos = Align2::RIGHT_CENTER
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
.min;
if ui.visuals().button_frame {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.rounding,
visuals.weak_bg_fill,
);
}
let text_color = visuals.text_color();
text_galley.paint_with_fallback_color(ui.painter(), text_pos, text_color);
icon_galley.paint_with_fallback_color(ui.painter(), icon_pos, text_color);
}
response
}
}
pub struct SubMenu {
button: SubMenuButton,
parent_state: Arc<RwLock<MenuState>>,
}
impl SubMenu {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
let index = parent_state.write().next_entry_index();
Self {
button: SubMenuButton::new(text, "", index),
parent_state,
}
}
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let sub_id = ui.id().with(self.button.index);
let button = self.button.show(ui, &self.parent_state.read(), sub_id);
self.parent_state
.write()
.submenu_button_interaction(ui, sub_id, &button);
let inner = self
.parent_state
.write()
.show_submenu(ui.ctx(), sub_id, add_contents);
InnerResponse::new(inner, button)
}
}
pub(crate) struct MenuState {
/// The opened sub-menu and its [`Id`]
sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
/// Bounding box of this menu (without the sub-menu)
pub rect: Rect,
/// Used to check if any menu in the tree wants to close
pub response: MenuResponse,
/// Used to hash different [`Id`]s for sub-menus
entry_count: usize,
}
impl MenuState {
pub fn new(position: Pos2) -> Self {
Self {
rect: Rect::from_min_size(position, Vec2::ZERO),
sub_menu: None,
response: MenuResponse::Stay,
entry_count: 0,
}
}
/// Close menu hierarchy.
pub fn close(&mut self) {
self.response = MenuResponse::Close;
}
pub fn show<R>(
ctx: &Context,
menu_state: &Arc<RwLock<Self>>,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
crate::menu::menu_ui(ctx, id, menu_state, add_contents)
}
fn show_submenu<R>(
&mut self,
ctx: &Context,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = Self::show(ctx, sub, id, add_contents);
(sub.read().response, inner_response.inner)
})?;
self.cascade_close_response(sub_response);
Some(response)
}
/// Check if position is in the menu hierarchy's area.
pub fn area_contains(&self, pos: Pos2) -> bool {
self.rect.contains(pos)
|| self
.sub_menu
.as_ref()
.map_or(false, |(_, sub)| sub.read().area_contains(pos))
}
fn next_entry_index(&mut self) -> usize {
self.entry_count += 1;
self.entry_count - 1
}
/// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(&pointer) {
// ensure to repaint once even when pointer is not moving
ui.ctx().request_repaint();
} else if !open && button.hovered() {
let pos = button.rect.right_top();
self.open_submenu(sub_id, pos);
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
self.close_submenu();
}
}
/// Check if `dir` points from `pos` towards left side of `rect`.
fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool {
let vel_a = dir.angle();
let top_a = (rect.left_top() - pos).angle();
let bottom_a = (rect.left_bottom() - pos).angle();
bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0
}
/// Check if pointer is moving towards current submenu.
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
if pointer.is_still() {
return false;
}
if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() {
return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect);
}
}
false
}
/// Check if pointer is hovering current submenu.
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() {
return sub_menu.read().area_contains(pos);
}
}
false
}
/// Cascade close response to menu root.
fn cascade_close_response(&mut self, response: MenuResponse) {
if response.is_close() {
self.response = response;
}
}
fn is_open(&self, id: Id) -> bool {
self.sub_id() == Some(id)
}
fn sub_id(&self) -> Option<Id> {
self.sub_menu.as_ref().map(|(id, _)| *id)
}
fn current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> {
self.sub_menu.as_ref().map(|(_, sub)| sub)
}
fn submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> {
self.sub_menu
.as_ref()
.and_then(|(k, sub)| if id == *k { Some(sub) } else { None })
}
/// Open submenu at position, if not already open.
fn open_submenu(&mut self, id: Id, pos: Pos2) {
if !self.is_open(id) {
self.sub_menu = Some((id, Arc::new(RwLock::new(MenuState::new(pos)))));
}
}
fn close_submenu(&mut self) {
self.sub_menu = None;
}
}

View file

@ -1,76 +0,0 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OperatingSystem {
/// Unknown OS - could be wasm
Unknown,
/// Android OS.
Android,
/// Apple iPhone OS.
IOS,
/// Linux or Unix other than Android.
Nix,
/// MacOS.
Mac,
/// Windows.
Windows,
}
impl Default for OperatingSystem {
fn default() -> Self {
Self::from_target_os()
}
}
impl OperatingSystem {
pub const fn from_target_os() -> Self {
if cfg!(target_arch = "wasm32") {
Self::Unknown
} else if cfg!(target_os = "android") {
Self::Android
} else if cfg!(target_os = "ios") {
Self::IOS
} else if cfg!(target_os = "macos") {
Self::Mac
} else if cfg!(target_os = "windows") {
Self::Android
} else if cfg!(target_os = "linux")
|| cfg!(target_os = "dragonfly")
|| cfg!(target_os = "freebsd")
|| cfg!(target_os = "netbsd")
|| cfg!(target_os = "openbsd")
{
Self::Nix
} else {
Self::Unknown
}
}
/// Helper: try to guess from the user-agent of a browser.
pub fn from_user_agent(user_agent: &str) -> Self {
if user_agent.contains("Android") {
Self::Android
} else if user_agent.contains("like Mac") {
Self::IOS
} else if user_agent.contains("Win") {
Self::Windows
} else if user_agent.contains("Mac") {
Self::Mac
} else if user_agent.contains("Linux")
|| user_agent.contains("X11")
|| user_agent.contains("Unix")
{
Self::Nix
} else {
#[cfg(feature = "tracing")]
tracing::warn!(
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
user_agent);
Self::Unknown
}
}
}

View file

@ -1,455 +0,0 @@
use std::ops::RangeInclusive;
use std::sync::Arc;
use crate::{
emath::{Align2, Pos2, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
use epaint::{
text::{Fonts, Galley},
CircleShape, RectShape, Rounding, Shape, Stroke,
};
/// Helper to paint shapes and text to a specific region on a specific layer.
///
/// All coordinates are screen coordinates in the unit points (one point can consist of many physical pixels).
#[derive(Clone)]
pub struct Painter {
/// Source of fonts and destination of shapes
ctx: Context,
/// Where we paint
layer_id: LayerId,
/// Everything painted in this [`Painter`] will be clipped against this.
/// This means nothing outside of this rectangle will be visible on screen.
clip_rect: Rect,
/// If set, all shapes will have their colors modified to be closer to this.
/// This is used to implement grayed out interfaces.
fade_to_color: Option<Color32>,
}
impl Painter {
/// Create a painter to a specific layer within a certain clip rectangle.
pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self {
Self {
ctx,
layer_id,
clip_rect,
fade_to_color: None,
}
}
/// Redirect where you are painting.
#[must_use]
pub fn with_layer_id(self, layer_id: LayerId) -> Self {
Self {
ctx: self.ctx,
layer_id,
clip_rect: self.clip_rect,
fade_to_color: None,
}
}
/// Create a painter for a sub-region of this [`Painter`].
///
/// The clip-rect of the returned [`Painter`] will be the intersection
/// of the given rectangle and the `clip_rect()` of the parent [`Painter`].
pub fn with_clip_rect(&self, rect: Rect) -> Self {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
}
}
/// Redirect where you are painting.
pub fn set_layer_id(&mut self, layer_id: LayerId) {
self.layer_id = layer_id;
}
/// If set, colors will be modified to look like this
pub(crate) fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}
pub(crate) fn is_visible(&self) -> bool {
self.fade_to_color != Some(Color32::TRANSPARENT)
}
/// If `false`, nothing added to the painter will be visible
pub(crate) fn set_invisible(&mut self) {
self.fade_to_color = Some(Color32::TRANSPARENT);
}
#[deprecated = "Use Painter::with_clip_rect"] // Deprecated in 2022-04-18, before egui 0.18
pub fn sub_region(&self, rect: Rect) -> Self {
Self {
ctx: self.ctx.clone(),
layer_id: self.layer_id,
clip_rect: rect.intersect(self.clip_rect),
fade_to_color: self.fade_to_color,
}
}
}
/// ## Accessors etc
impl Painter {
/// Get a reference to the parent [`Context`].
#[inline(always)]
pub fn ctx(&self) -> &Context {
&self.ctx
}
/// Read-only access to the shared [`Fonts`].
///
/// See [`Context`] documentation for how locks work.
#[inline(always)]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
self.ctx.fonts(reader)
}
/// Where we paint
#[inline(always)]
pub fn layer_id(&self) -> LayerId {
self.layer_id
}
/// Everything painted in this [`Painter`] will be clipped against this.
/// This means nothing outside of this rectangle will be visible on screen.
#[inline(always)]
pub fn clip_rect(&self) -> Rect {
self.clip_rect
}
/// Everything painted in this [`Painter`] will be clipped against this.
/// This means nothing outside of this rectangle will be visible on screen.
#[inline(always)]
pub fn set_clip_rect(&mut self, clip_rect: Rect) {
self.clip_rect = clip_rect;
}
/// Useful for pixel-perfect rendering.
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
self.ctx().round_to_pixel(point)
}
/// Useful for pixel-perfect rendering.
#[inline(always)]
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
self.ctx().round_vec_to_pixels(vec)
}
/// Useful for pixel-perfect rendering.
#[inline(always)]
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
self.ctx().round_pos_to_pixels(pos)
}
}
/// ## Low level
impl Painter {
#[inline]
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
}
fn transform_shape(&self, shape: &mut Shape) {
if let Some(fade_to_color) = self.fade_to_color {
tint_shape_towards(shape, fade_to_color);
}
}
/// It is up to the caller to make sure there is room for this.
/// Can be used for free painting.
/// NOTE: all coordinates are screen coordinates!
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
} else {
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list(|l| l.add(self.clip_rect, shape))
}
}
/// Add many shapes at once.
///
/// Calling this once is generally faster than calling [`Self::add`] multiple times.
pub fn extend<I: IntoIterator<Item = Shape>>(&self, shapes: I) {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
return;
}
if self.fade_to_color.is_some() {
let shapes = shapes.into_iter().map(|mut shape| {
self.transform_shape(&mut shape);
shape
});
self.paint_list(|l| l.extend(self.clip_rect, shapes));
} else {
self.paint_list(|l| l.extend(self.clip_rect, shapes));
};
}
/// Modify an existing [`Shape`].
pub fn set(&self, idx: ShapeIdx, shape: impl Into<Shape>) {
if self.fade_to_color == Some(Color32::TRANSPARENT) {
return;
}
let mut shape = shape.into();
self.transform_shape(&mut shape);
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
}
}
/// ## Debug painting
impl Painter {
#[allow(clippy::needless_pass_by_value)]
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
self.rect(
rect,
0.0,
color.additive().linear_multiply(0.015),
(1.0, color),
);
self.text(
rect.min,
Align2::LEFT_TOP,
text.to_string(),
FontId::monospace(12.0),
color,
);
}
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
let color = self.ctx.style().visuals.error_fg_color;
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text))
}
/// text with a background
#[allow(clippy::needless_pass_by_value)]
pub fn debug_text(
&self,
pos: Pos2,
anchor: Align2,
color: Color32,
text: impl ToString,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(12.0), color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
let frame_rect = rect.expand(2.0);
self.add(Shape::rect_filled(
frame_rect,
0.0,
Color32::from_black_alpha(150),
));
self.galley(rect.min, galley);
frame_rect
}
}
/// # Paint different primitives
impl Painter {
/// Paints a line from the first point to the second.
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
self.add(Shape::LineSegment {
points,
stroke: stroke.into(),
});
}
/// Paints a horizontal line.
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) {
self.add(Shape::hline(x, y, stroke));
}
/// Paints a vertical line.
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) {
self.add(Shape::vline(x, y, stroke));
}
pub fn circle(
&self,
center: Pos2,
radius: f32,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: stroke.into(),
});
}
pub fn circle_filled(&self, center: Pos2, radius: f32, fill_color: impl Into<Color32>) {
self.add(CircleShape {
center,
radius,
fill: fill_color.into(),
stroke: Default::default(),
});
}
pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) {
self.add(CircleShape {
center,
radius,
fill: Default::default(),
stroke: stroke.into(),
});
}
pub fn rect(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: stroke.into(),
});
}
pub fn rect_filled(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
fill_color: impl Into<Color32>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: fill_color.into(),
stroke: Default::default(),
});
}
pub fn rect_stroke(
&self,
rect: Rect,
rounding: impl Into<Rounding>,
stroke: impl Into<Stroke>,
) {
self.add(RectShape {
rect,
rounding: rounding.into(),
fill: Default::default(),
stroke: stroke.into(),
});
}
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: Stroke) {
use crate::emath::*;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = vec.length() / 4.0;
let tip = origin + vec;
let dir = vec.normalized();
self.line_segment([origin, tip], stroke);
self.line_segment([tip, tip - tip_length * (rot * dir)], stroke);
self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke);
}
/// An image at the given position.
///
/// `uv` should normally be `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`
/// unless you want to crop or flip the image.
///
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
self.add(Shape::image(texture_id, rect, uv, tint));
}
}
/// ## Text
impl Painter {
/// Lay out and paint some text.
///
/// To center the text at the given position, use `Align2::CENTER_CENTER`.
///
/// To find out the size of text before painting it, use
/// [`Self::layout`] or [`Self::layout_no_wrap`].
///
/// Returns where the text ended up.
#[allow(clippy::needless_pass_by_value)]
pub fn text(
&self,
pos: Pos2,
anchor: Align2,
text: impl ToString,
font_id: FontId,
text_color: Color32,
) -> Rect {
let galley = self.layout_no_wrap(text.to_string(), font_id, text_color);
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
self.galley(rect.min, galley);
rect
}
/// Will wrap text at the given width and line break at `\n`.
///
/// Paint the results with [`Self::galley`].
#[inline(always)]
pub fn layout(
&self,
text: String,
font_id: FontId,
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
}
/// Will line break at `\n`.
///
/// Paint the results with [`Self::galley`].
#[inline(always)]
pub fn layout_no_wrap(
&self,
text: String,
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
}
/// Paint text that has already been layed out in a [`Galley`].
///
/// You can create the [`Galley`] with [`Self::layout`].
///
/// If you want to change the color of the text, use [`Self::galley_with_color`].
#[inline(always)]
pub fn galley(&self, pos: Pos2, galley: Arc<Galley>) {
if !galley.is_empty() {
self.add(Shape::galley(pos, galley));
}
}
/// Paint text that has already been layed out in a [`Galley`].
///
/// You can create the [`Galley`] with [`Self::layout`].
///
/// The text color in the [`Galley`] will be replaced with the given color.
#[inline(always)]
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(Shape::galley_with_color(pos, galley, text_color));
}
}
}
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
epaint::shape_transform::adjust_colors(shape, &|color| {
*color = crate::ecolor::tint_color_towards(*color, target);
});
}

View file

@ -1,819 +0,0 @@
use crate::{
emath::{Align, Pos2, Rect, Vec2},
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
NUM_POINTER_BUTTONS,
};
// ----------------------------------------------------------------------------
/// The result of adding a widget to a [`Ui`].
///
/// A [`Response`] lets you know whether or not a widget is being hovered, clicked or dragged.
/// It also lets you easily show a tooltip on hover.
///
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
// TODO(emilk): we should be using bit sets instead of so many bools
#[derive(Clone)]
pub struct Response {
// CONTEXT:
/// Used for optionally showing a tooltip and checking for more interactions.
pub ctx: Context,
// IN:
/// Which layer the widget is part of.
pub layer_id: LayerId,
/// The [`Id`] of the widget/area this response pertains.
pub id: Id,
/// The area of the screen we are talking about.
pub rect: Rect,
/// The senses (click and/or drag) that the widget was interested in (if any).
pub sense: Sense,
/// Was the widget enabled?
/// If `false`, there was no interaction attempted (not even hover).
#[doc(hidden)]
pub enabled: bool,
// OUT:
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
#[doc(hidden)]
pub hovered: bool,
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
pub highlighted: bool,
/// The pointer clicked this thing this frame.
#[doc(hidden)]
pub clicked: [bool; NUM_POINTER_BUTTONS],
// TODO(emilk): `released` for sliders
/// The thing was double-clicked.
#[doc(hidden)]
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
/// The thing was triple-clicked.
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
/// The widgets is being dragged
#[doc(hidden)]
pub dragged: bool,
/// The widget was being dragged, but now it has been released.
#[doc(hidden)]
pub drag_released: bool,
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
#[doc(hidden)]
pub is_pointer_button_down_on: bool,
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
#[doc(hidden)]
pub interact_pointer_pos: Option<Pos2>,
/// What the underlying data changed?
///
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
/// Always `false` for something like a [`Button`](crate::Button).
#[doc(hidden)]
pub changed: bool,
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
ctx: _,
layer_id,
id,
rect,
sense,
enabled,
hovered,
highlighted,
clicked,
double_clicked,
triple_clicked,
dragged,
drag_released,
is_pointer_button_down_on,
interact_pointer_pos,
changed,
} = self;
f.debug_struct("Response")
.field("layer_id", layer_id)
.field("id", id)
.field("rect", rect)
.field("sense", sense)
.field("enabled", enabled)
.field("hovered", hovered)
.field("highlighted", highlighted)
.field("clicked", clicked)
.field("double_clicked", double_clicked)
.field("triple_clicked", triple_clicked)
.field("dragged", dragged)
.field("drag_released", drag_released)
.field("is_pointer_button_down_on", is_pointer_button_down_on)
.field("interact_pointer_pos", interact_pointer_pos)
.field("changed", changed)
.finish()
}
}
impl Response {
/// Returns true if this widget was clicked this frame by the primary button.
///
/// A click is registered when the mouse or touch is released within
/// a certain amount of time and distance from when and where it was pressed.
///
/// Note that the widget must be sensing clicks with [`Sense::click`].
/// [`crate::Button`] senses clicks; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
///
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
#[inline(always)]
pub fn clicked(&self) -> bool {
self.clicked[PointerButton::Primary as usize]
}
/// Returns true if this widget was clicked this frame by the given button.
pub fn clicked_by(&self, button: PointerButton) -> bool {
self.clicked[button as usize]
}
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
pub fn secondary_clicked(&self) -> bool {
self.clicked[PointerButton::Secondary as usize]
}
/// Returns true if this widget was clicked this frame by the middle mouse button.
pub fn middle_clicked(&self) -> bool {
self.clicked[PointerButton::Middle as usize]
}
/// Returns true if this widget was double-clicked this frame by the primary button.
pub fn double_clicked(&self) -> bool {
self.double_clicked[PointerButton::Primary as usize]
}
/// Returns true if this widget was triple-clicked this frame by the primary button.
pub fn triple_clicked(&self) -> bool {
self.triple_clicked[PointerButton::Primary as usize]
}
/// Returns true if this widget was double-clicked this frame by the given button.
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
self.double_clicked[button as usize]
}
/// Returns true if this widget was triple-clicked this frame by the given button.
pub fn triple_clicked_by(&self, button: PointerButton) -> bool {
self.triple_clicked[button as usize]
}
/// `true` if there was a click *outside* this widget this frame.
pub fn clicked_elsewhere(&self) -> bool {
// We do not use self.clicked(), because we want to catch all clicks within our frame,
// even if we aren't clickable (or even enabled).
// This is important for windows and such that should close then the user clicks elsewhere.
self.ctx.input(|i| {
let pointer = &i.pointer;
if pointer.any_click() {
// We detect clicks/hover on a "interact_rect" that is slightly larger than
// self.rect. See Context::interact.
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
// hence the extra complexity here.
if self.hovered() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
} else {
false // clicked without a pointer, weird
}
} else {
false
}
})
}
/// Was the widget enabled?
/// If false, there was no interaction attempted
/// and the widget should be drawn in a gray disabled look.
#[inline(always)]
pub fn enabled(&self) -> bool {
self.enabled
}
/// The pointer is hovering above this widget or the widget was clicked/tapped this frame.
///
/// Note that this is slightly different from checking `response.rect.contains(pointer_pos)`.
/// For one, the hover rectangle is slightly larger, by half of the current item spacing
/// (to make it easier to click things). But `hovered` also checks that no other area
/// is covering this response rectangle.
#[inline(always)]
pub fn hovered(&self) -> bool {
self.hovered
}
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
#[doc(hidden)]
pub fn highlighted(&self) -> bool {
self.highlighted
}
/// This widget has the keyboard focus (i.e. is receiving key presses).
///
/// This function only returns true if the UI as a whole (e.g. window)
/// also has the keyboard focus. That makes this function suitable
/// for style choices, e.g. a thicker border around focused widgets.
pub fn has_focus(&self) -> bool {
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
}
/// True if this widget has keyboard focus this frame, but didn't last frame.
pub fn gained_focus(&self) -> bool {
self.ctx.memory(|mem| mem.gained_focus(self.id))
}
/// The widget had keyboard focus and lost it,
/// either because the user pressed tab or clicked somewhere else,
/// or (in case of a [`crate::TextEdit`]) because the user pressed enter.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_text = String::new();
/// # fn do_request(_: &str) {}
/// let response = ui.text_edit_singleline(&mut my_text);
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
/// do_request(&my_text);
/// }
/// # });
/// ```
pub fn lost_focus(&self) -> bool {
self.ctx.memory(|mem| mem.lost_focus(self.id))
}
/// Request that this widget get keyboard focus.
pub fn request_focus(&self) {
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
}
/// Surrender keyboard focus for this widget.
pub fn surrender_focus(&self) {
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
}
/// The widgets is being dragged.
///
/// To find out which button(s), query [`crate::PointerState::button_down`]
/// (`ui.input(|i| i.pointer.button_down(…))`).
///
/// Note that the widget must be sensing drags with [`Sense::drag`].
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
///
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
#[inline(always)]
pub fn dragged(&self) -> bool {
self.dragged
}
pub fn dragged_by(&self, button: PointerButton) -> bool {
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
}
/// Did a drag on this widgets begin this frame?
pub fn drag_started(&self) -> bool {
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
}
/// Did a drag on this widgets by the button begin this frame?
pub fn drag_started_by(&self, button: PointerButton) -> bool {
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
}
/// The widget was being dragged, but now it has been released.
pub fn drag_released(&self) -> bool {
self.drag_released
}
/// The widget was being dragged by the button, but now it has been released.
pub fn drag_released_by(&self, button: PointerButton) -> bool {
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// If dragged, how many points were we dragged and in what direction?
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
self.ctx.input(|i| i.pointer.delta())
} else {
Vec2::ZERO
}
}
/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
pub fn interact_pointer_pos(&self) -> Option<Pos2> {
self.interact_pointer_pos
}
/// If it is a good idea to show a tooltip, where is pointer?
/// None if the pointer is outside the response area.
pub fn hover_pos(&self) -> Option<Pos2> {
if self.hovered() {
self.ctx.input(|i| i.pointer.hover_pos())
} else {
None
}
}
/// Is the pointer button currently down on this widget?
/// This is true if the pointer is pressing down or dragging a widget
#[inline(always)]
pub fn is_pointer_button_down_on(&self) -> bool {
self.is_pointer_button_down_on
}
/// What the underlying data changed?
///
/// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc.
/// Always `false` for something like a [`Button`](crate::Button).
///
/// Can sometimes be `true` even though the data didn't changed
/// (e.g. if the user entered a character and erased it the same frame).
///
/// This is not set if the *view* of the data was changed.
/// For instance, moving the cursor in a [`TextEdit`](crate::TextEdit) does not set this to `true`.
#[inline(always)]
pub fn changed(&self) -> bool {
self.changed
}
/// Report the data shown by this widget changed.
///
/// This must be called by widgets that represent some mutable data,
/// e.g. checkboxes, sliders etc.
///
/// This should be called when the *content* changes, but not when the view does.
/// So we call this when the text of a [`crate::TextEdit`], but not when the cursors changes.
#[inline(always)]
pub fn mark_changed(&mut self) {
self.changed = true;
}
/// Show this UI if the widget was hovered (i.e. a tooltip).
///
/// The text will not be visible if the widget is not enabled.
/// For that, use [`Self::on_disabled_hover_ui`] instead.
///
/// If you call this multiple times the tooltips will stack underneath the previous ones.
#[doc(alias = "tooltip")]
pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.should_show_hover_ui() {
crate::containers::show_tooltip_for(
&self.ctx,
self.id.with("__tooltip"),
&self.rect,
add_contents,
);
}
self
}
/// Show this UI when hovering if the widget is disabled.
pub fn on_disabled_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if !self.enabled && self.ctx.rect_contains_pointer(self.layer_id, self.rect) {
crate::containers::show_tooltip_for(
&self.ctx,
self.id.with("__tooltip"),
&self.rect,
add_contents,
);
}
self
}
/// Like `on_hover_ui`, but show the ui next to cursor.
pub fn on_hover_ui_at_pointer(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.should_show_hover_ui() {
crate::containers::show_tooltip_at_pointer(
&self.ctx,
self.id.with("__tooltip"),
add_contents,
);
}
self
}
/// Was the tooltip open last frame?
pub fn is_tooltip_open(&self) -> bool {
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id.with("__tooltip"))
}
fn should_show_hover_ui(&self) -> bool {
if self.ctx.memory(|mem| mem.everything_is_visible()) {
return true;
}
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
return false;
}
if self.ctx.style().interaction.show_tooltips_only_when_still {
// We only show the tooltip when the mouse pointer is still,
// but once shown we keep showing it until the mouse leaves the parent.
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
// wait for mouse to stop
self.ctx.request_repaint();
return false;
}
}
// We don't want tooltips of things while we are dragging them,
// but we do want tooltips while holding down on an item on a touch screen.
if self
.ctx
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
{
return false;
}
true
}
/// Like `on_hover_text`, but show the text next to cursor.
#[doc(alias = "tooltip")]
pub fn on_hover_text_at_pointer(self, text: impl Into<WidgetText>) -> Self {
self.on_hover_ui_at_pointer(|ui| {
ui.add(crate::widgets::Label::new(text));
})
}
/// Show this text if the widget was hovered (i.e. a tooltip).
///
/// The text will not be visible if the widget is not enabled.
/// For that, use [`Self::on_disabled_hover_text`] instead.
///
/// If you call this multiple times the tooltips will stack underneath the previous ones.
#[doc(alias = "tooltip")]
pub fn on_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_hover_ui(|ui| {
ui.add(crate::widgets::Label::new(text));
})
}
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
///
/// The highlight takes on frame to take effect if you call this after the widget has been fully rendered.
///
/// See also [`Context::highlight_widget`].
pub fn highlight(mut self) -> Self {
self.ctx.highlight_widget(self.id);
self.highlighted = true;
self
}
/// Show this text when hovering if the widget is disabled.
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
self.on_disabled_hover_ui(|ui| {
ui.add(crate::widgets::Label::new(text));
})
}
/// When hovered, use this icon for the mouse cursor.
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() {
self.ctx.set_cursor_icon(cursor);
}
self
}
/// When hovered or dragged, use this icon for the mouse cursor.
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
if self.hovered() || self.dragged() {
self.ctx.set_cursor_icon(cursor);
}
self
}
/// Check for more interactions (e.g. sense clicks on a [`Response`] returned from a label).
///
/// Note that this call will not add any hover-effects to the widget, so when possible
/// it is better to give the widget a [`Sense`] instead, e.g. using [`crate::Label::sense`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let response = ui.label("hello");
/// assert!(!response.clicked()); // labels don't sense clicks by default
/// let response = response.interact(egui::Sense::click());
/// if response.clicked() { /* … */ }
/// # });
/// ```
#[must_use]
pub fn interact(&self, sense: Sense) -> Self {
self.ctx.interact_with_hovered(
self.layer_id,
self.id,
self.rect,
sense,
self.enabled,
self.hovered,
)
}
/// Adjust the scroll position until this UI becomes visible.
///
/// If `align` is `None`, it'll scroll enough to bring the UI into view.
///
/// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::ScrollArea::vertical().show(ui, |ui| {
/// for i in 0..1000 {
/// let response = ui.button("Scroll to me");
/// if response.clicked() {
/// response.scroll_to_me(Some(egui::Align::Center));
/// }
/// }
/// });
/// # });
/// ```
pub fn scroll_to_me(&self, align: Option<Align>) {
self.ctx.frame_state_mut(|state| {
state.scroll_target[0] = Some((self.rect.x_range(), align));
state.scroll_target[1] = Some((self.rect.y_range(), align));
});
}
/// For accessibility.
///
/// Call after interacting and potential calls to [`Self::mark_changed`].
pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) {
use crate::output::OutputEvent;
let event = if self.clicked() {
Some(OutputEvent::Clicked(make_info()))
} else if self.double_clicked() {
Some(OutputEvent::DoubleClicked(make_info()))
} else if self.triple_clicked() {
Some(OutputEvent::TripleClicked(make_info()))
} else if self.gained_focus() {
Some(OutputEvent::FocusGained(make_info()))
} else if self.changed {
Some(OutputEvent::ValueChanged(make_info()))
} else {
None
};
if let Some(event) = event {
self.output_event(event);
} else {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, make_info());
});
}
}
pub fn output_event(&self, event: crate::output::OutputEvent) {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| {
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
});
self.ctx.output_mut(|o| o.events.push(event));
}
#[cfg(feature = "accesskit")]
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
builder.set_bounds(accesskit::Rect {
x0: self.rect.min.x.into(),
y0: self.rect.min.y.into(),
x1: self.rect.max.x.into(),
y1: self.rect.max.y.into(),
});
if self.sense.focusable {
builder.add_action(accesskit::Action::Focus);
}
if self.sense.click && builder.default_action_verb().is_none() {
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
}
}
#[cfg(feature = "accesskit")]
fn fill_accesskit_node_from_widget_info(
&self,
builder: &mut accesskit::NodeBuilder,
info: crate::WidgetInfo,
) {
use crate::WidgetType;
use accesskit::{CheckedState, Role};
self.fill_accesskit_node_common(builder);
builder.set_role(match info.typ {
WidgetType::Label => Role::StaticText,
WidgetType::Link => Role::Link,
WidgetType::TextEdit => Role::TextField,
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
Role::Button
}
WidgetType::Checkbox => Role::CheckBox,
WidgetType::RadioButton => Role::RadioButton,
WidgetType::SelectableLabel => Role::ToggleButton,
WidgetType::ComboBox => Role::PopupButton,
WidgetType::Slider => Role::Slider,
WidgetType::DragValue => Role::SpinButton,
WidgetType::ColorButton => Role::ColorWell,
WidgetType::Other => Role::Unknown,
});
if let Some(label) = info.label {
builder.set_name(label);
}
if let Some(value) = info.current_text_value {
builder.set_value(value);
}
if let Some(value) = info.value {
builder.set_numeric_value(value);
}
if let Some(selected) = info.selected {
builder.set_checked_state(if selected {
CheckedState::True
} else {
CheckedState::False
});
}
}
/// Associate a label with a control for accessibility.
///
/// # Example
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut text = "Arthur".to_string();
/// ui.horizontal(|ui| {
/// let label = ui.label("Your name: ");
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
/// });
/// # });
/// ```
pub fn labelled_by(self, id: Id) -> Self {
#[cfg(feature = "accesskit")]
self.ctx.accesskit_node_builder(self.id, |builder| {
builder.push_labelled_by(id.accesskit_id());
});
#[cfg(not(feature = "accesskit"))]
{
let _ = id;
}
self
}
/// Response to secondary clicks (right-clicks) by showing the given menu.
///
/// ```
/// # use egui::{Label, Sense};
/// # egui::__run_test_ui(|ui| {
/// let response = ui.add(Label::new("Right-click me!").sense(Sense::click()));
/// response.context_menu(|ui| {
/// if ui.button("Close the menu").clicked() {
/// ui.close_menu();
/// }
/// });
/// # });
/// ```
///
/// See also: [`Ui::menu_button`] and [`Ui::close_menu`].
pub fn context_menu(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
menu::context_menu(&self, add_contents);
self
}
}
impl Response {
/// A logical "or" operation.
/// For instance `a.union(b).hovered` means "was either a or b hovered?".
///
/// The resulting [`Self::id`] will come from the first (`self`) argument.
pub fn union(&self, other: Self) -> Self {
assert!(self.ctx == other.ctx);
crate::egui_assert!(
self.layer_id == other.layer_id,
"It makes no sense to combine Responses from two different layers"
);
Self {
ctx: other.ctx,
layer_id: self.layer_id,
id: self.id,
rect: self.rect.union(other.rect),
sense: self.sense.union(other.sense),
enabled: self.enabled || other.enabled,
hovered: self.hovered || other.hovered,
highlighted: self.highlighted || other.highlighted,
clicked: [
self.clicked[0] || other.clicked[0],
self.clicked[1] || other.clicked[1],
self.clicked[2] || other.clicked[2],
self.clicked[3] || other.clicked[3],
self.clicked[4] || other.clicked[4],
],
double_clicked: [
self.double_clicked[0] || other.double_clicked[0],
self.double_clicked[1] || other.double_clicked[1],
self.double_clicked[2] || other.double_clicked[2],
self.double_clicked[3] || other.double_clicked[3],
self.double_clicked[4] || other.double_clicked[4],
],
triple_clicked: [
self.triple_clicked[0] || other.triple_clicked[0],
self.triple_clicked[1] || other.triple_clicked[1],
self.triple_clicked[2] || other.triple_clicked[2],
self.triple_clicked[3] || other.triple_clicked[3],
self.triple_clicked[4] || other.triple_clicked[4],
],
dragged: self.dragged || other.dragged,
drag_released: self.drag_released || other.drag_released,
is_pointer_button_down_on: self.is_pointer_button_down_on
|| other.is_pointer_button_down_on,
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
changed: self.changed || other.changed,
}
}
}
impl Response {
/// Returns a response with a modified [`Self::rect`].
pub fn with_new_rect(self, rect: Rect) -> Self {
Self { rect, ..self }
}
}
/// To summarize the response from many widgets you can use this pattern:
///
/// ```
/// use egui::*;
/// fn draw_vec2(ui: &mut Ui, v: &mut Vec2) -> Response {
/// ui.add(DragValue::new(&mut v.x)) | ui.add(DragValue::new(&mut v.y))
/// }
/// ```
///
/// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered.
impl std::ops::BitOr for Response {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
/// To summarize the response from many widgets you can use this pattern:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let (widget_a, widget_b, widget_c) = (egui::Label::new("a"), egui::Label::new("b"), egui::Label::new("c"));
/// let mut response = ui.add(widget_a);
/// response |= ui.add(widget_b);
/// response |= ui.add(widget_c);
/// if response.hovered() { ui.label("You hovered at least one of the widgets"); }
/// # });
/// ```
impl std::ops::BitOrAssign for Response {
fn bitor_assign(&mut self, rhs: Self) {
*self = self.union(rhs);
}
}
// ----------------------------------------------------------------------------
/// Returned when we wrap some ui-code and want to return both
/// the results of the inner function and the ui as a whole, e.g.:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let inner_resp = ui.horizontal(|ui| {
/// ui.label("Blah blah");
/// 42
/// });
/// inner_resp.response.on_hover_text("You hovered the horizontal layout");
/// assert_eq!(inner_resp.inner, 42);
/// # });
/// ```
#[derive(Debug)]
pub struct InnerResponse<R> {
/// What the user closure returned.
pub inner: R,
/// The response of the area.
pub response: Response,
}
impl<R> InnerResponse<R> {
#[inline]
pub fn new(inner: R, response: Response) -> Self {
Self { inner, response }
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,164 +0,0 @@
//! Computing the same thing each frame can be expensive,
//! so often you want to save the result from the previous frame and reuse it.
//!
//! Enter [`FrameCache`]: it caches the results of a computation for one frame.
//! If it is still used next frame, it is not recomputed.
//! If it is not used next frame, it is evicted from the cache to save memory.
/// Something that does an expensive computation that we want to cache
/// to save us from recomputing it each frame.
pub trait ComputerMut<Key, Value>: 'static + Send + Sync {
fn compute(&mut self, key: Key) -> Value;
}
/// Caches the results of a computation for one frame.
/// If it is still used next frame, it is not recomputed.
/// If it is not used next frame, it is evicted from the cache to save memory.
pub struct FrameCache<Value, Computer> {
generation: u32,
computer: Computer,
cache: nohash_hasher::IntMap<u64, (u32, Value)>,
}
impl<Value, Computer> Default for FrameCache<Value, Computer>
where
Computer: Default,
{
fn default() -> Self {
Self::new(Computer::default())
}
}
impl<Value, Computer> FrameCache<Value, Computer> {
pub fn new(computer: Computer) -> Self {
Self {
generation: 0,
computer,
cache: Default::default(),
}
}
/// Must be called once per frame to clear the cache.
pub fn evice_cache(&mut self) {
let current_generation = self.generation;
self.cache.retain(|_key, cached| {
cached.0 == current_generation // only keep those that were used this frame
});
self.generation = self.generation.wrapping_add(1);
}
}
impl<Value, Computer> FrameCache<Value, Computer> {
/// Get from cache (if the same key was used last frame)
/// or recompute and store in the cache.
pub fn get<Key>(&mut self, key: Key) -> Value
where
Key: Copy + std::hash::Hash,
Value: Clone,
Computer: ComputerMut<Key, Value>,
{
let hash = crate::util::hash(key);
match self.cache.entry(hash) {
std::collections::hash_map::Entry::Occupied(entry) => {
let cached = entry.into_mut();
cached.0 = self.generation;
cached.1.clone()
}
std::collections::hash_map::Entry::Vacant(entry) => {
let value = self.computer.compute(key);
entry.insert((self.generation, value.clone()));
value
}
}
}
}
#[allow(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
/// Call once per frame to evict cache.
fn update(&mut self);
/// Number of values currently in the cache.
fn len(&self) -> usize;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}
impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
for FrameCache<Value, Computer>
{
fn update(&mut self) {
self.evice_cache();
}
fn len(&self) -> usize {
self.cache.len()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
/// ```
/// use egui::util::cache::{CacheStorage, ComputerMut, FrameCache};
///
/// #[derive(Default)]
/// struct CharCounter {}
/// impl ComputerMut<&str, usize> for CharCounter {
/// fn compute(&mut self, s: &str) -> usize {
/// s.chars().count()
/// }
/// }
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// ```
#[derive(Default)]
pub struct CacheStorage {
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
}
impl CacheStorage {
pub fn cache<FrameCache: CacheTrait + Default>(&mut self) -> &mut FrameCache {
self.caches
.entry(std::any::TypeId::of::<FrameCache>())
.or_insert_with(|| Box::new(FrameCache::default()))
.as_any_mut()
.downcast_mut::<FrameCache>()
.unwrap()
}
/// Total number of cached values
fn num_values(&self) -> usize {
self.caches.values().map(|cache| cache.len()).sum()
}
/// Call once per frame to evict cache.
pub fn update(&mut self) {
for cache in self.caches.values_mut() {
cache.update();
}
}
}
impl Clone for CacheStorage {
fn clone(&self) -> Self {
// We return an empty cache that can be filled in again.
Self::default()
}
}
impl std::fmt::Debug for CacheStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FrameCacheStorage[{} caches with {} elements]",
self.caches.len(),
self.num_values()
)
}
}

View file

@ -1,41 +0,0 @@
use epaint::util::hash;
const FIXED_CACHE_SIZE: usize = 1024; // must be small for web/WASM build (for unknown reason)
/// Very stupid/simple key-value cache. TODO(emilk): improve
#[derive(Clone)]
pub(crate) struct FixedCache<K, V>([Option<(K, V)>; FIXED_CACHE_SIZE]);
impl<K, V> Default for FixedCache<K, V>
where
K: Copy,
V: Copy,
{
fn default() -> Self {
Self([None; FIXED_CACHE_SIZE])
}
}
impl<K, V> std::fmt::Debug for FixedCache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Cache")
}
}
impl<K, V> FixedCache<K, V>
where
K: std::hash::Hash + PartialEq,
{
pub fn get(&self, key: &K) -> Option<&V> {
let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize;
match &self.0[bucket] {
Some((k, v)) if k == key => Some(v),
_ => None,
}
}
pub fn set(&mut self, key: K, value: V) {
let bucket = (hash(&key) % (FIXED_CACHE_SIZE as u64)) as usize;
self.0[bucket] = Some((key, value));
}
}

View file

@ -1,720 +0,0 @@
// TODO(emilk): it is possible we can simplify `Element` further by
// assuming everything is possibly serializable, and by supplying serialize/deserialize functions for them.
// For non-serializable types, these simply return `None`.
// This will also allow users to pick their own serialization format per type.
use std::any::Any;
use std::sync::Arc;
// -----------------------------------------------------------------------------------------------
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
pub struct TypeId(u64);
impl TypeId {
#[inline]
pub fn of<T: Any + 'static>() -> Self {
std::any::TypeId::of::<T>().into()
}
#[inline(always)]
pub(crate) fn value(&self) -> u64 {
self.0
}
}
impl From<std::any::TypeId> for TypeId {
#[inline]
fn from(id: std::any::TypeId) -> Self {
Self(epaint::util::hash(id))
}
}
// -----------------------------------------------------------------------------------------------
#[cfg(feature = "persistence")]
pub trait SerializableAny:
'static + Any + Clone + serde::Serialize + for<'a> serde::Deserialize<'a> + Send + Sync
{
}
#[cfg(feature = "persistence")]
impl<T> SerializableAny for T where
T: 'static + Any + Clone + serde::Serialize + for<'a> serde::Deserialize<'a> + Send + Sync
{
}
#[cfg(not(feature = "persistence"))]
pub trait SerializableAny: 'static + Any + Clone + for<'a> Send + Sync {}
#[cfg(not(feature = "persistence"))]
impl<T> SerializableAny for T where T: 'static + Any + Clone + for<'a> Send + Sync {}
// -----------------------------------------------------------------------------------------------
#[cfg(feature = "persistence")]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
struct SerializedElement {
type_id: TypeId,
ron: Arc<str>,
}
#[cfg(feature = "persistence")]
type Serializer = fn(&Box<dyn Any + 'static + Send + Sync>) -> Option<String>;
enum Element {
/// A value, maybe serializable.
Value {
/// The actual value.
value: Box<dyn Any + 'static + Send + Sync>,
/// How to clone the value.
clone_fn: fn(&Box<dyn Any + 'static + Send + Sync>) -> Box<dyn Any + 'static + Send + Sync>,
/// How to serialize the value.
/// None if non-serializable type.
#[cfg(feature = "persistence")]
serialize_fn: Option<Serializer>,
},
/// A serialized value
Serialized {
/// The type of value we are storing.
type_id: TypeId,
/// The ron data we can deserialize.
ron: Arc<str>,
},
}
impl Clone for Element {
fn clone(&self) -> Self {
match &self {
Self::Value {
value,
clone_fn,
#[cfg(feature = "persistence")]
serialize_fn,
} => Self::Value {
value: clone_fn(value),
clone_fn: *clone_fn,
#[cfg(feature = "persistence")]
serialize_fn: *serialize_fn,
},
Self::Serialized { type_id, ron } => Self::Serialized {
type_id: *type_id,
ron: ron.clone(),
},
}
}
}
impl std::fmt::Debug for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Self::Value { value, .. } => f
.debug_struct("MaybeSerializable::Value")
.field("type_id", &value.type_id())
.finish_non_exhaustive(),
Self::Serialized { type_id, ron } => f
.debug_struct("MaybeSerializable::Serialized")
.field("type_id", &type_id)
.field("ron", &ron)
.finish(),
}
}
}
impl Element {
/// Create a value that won't be persisted.
#[inline]
pub(crate) fn new_temp<T: 'static + Any + Clone + Send + Sync>(t: T) -> Self {
Self::Value {
value: Box::new(t),
clone_fn: |x| {
let x = x.downcast_ref::<T>().unwrap(); // This unwrap will never panic, because we always construct this type using this `new` function and because we return &mut reference only with this type `T`, so type cannot change.
Box::new(x.clone())
},
#[cfg(feature = "persistence")]
serialize_fn: None,
}
}
/// Create a value that will be persisted.
#[inline]
pub(crate) fn new_persisted<T: SerializableAny>(t: T) -> Self {
Self::Value {
value: Box::new(t),
clone_fn: |x| {
let x = x.downcast_ref::<T>().unwrap(); // This unwrap will never panic, because we always construct this type using this `new` function and because we return &mut reference only with this type `T`, so type cannot change.
Box::new(x.clone())
},
#[cfg(feature = "persistence")]
serialize_fn: Some(|x| {
let x = x.downcast_ref::<T>().unwrap(); // This will never panic too, for same reason.
ron::to_string(x).ok()
}),
}
}
/// The type of the stored value.
#[inline]
pub(crate) fn type_id(&self) -> TypeId {
match self {
Self::Value { value, .. } => (**value).type_id().into(),
Self::Serialized { type_id, .. } => *type_id,
}
}
#[inline]
pub(crate) fn get_mut_temp<T: 'static>(&mut self) -> Option<&mut T> {
match self {
Self::Value { value, .. } => value.downcast_mut(),
Self::Serialized { .. } => None,
}
}
#[inline]
pub(crate) fn get_temp_mut_or_insert_with<T: 'static + Any + Clone + Send + Sync>(
&mut self,
insert_with: impl FnOnce() -> T,
) -> &mut T {
match self {
Self::Value { value, .. } => {
if !value.is::<T>() {
*self = Self::new_temp(insert_with());
}
}
Self::Serialized { .. } => {
*self = Self::new_temp(insert_with());
}
}
match self {
Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type
Self::Serialized { .. } => unreachable!(),
}
}
#[inline]
pub(crate) fn get_persisted_mut_or_insert_with<T: SerializableAny>(
&mut self,
insert_with: impl FnOnce() -> T,
) -> &mut T {
match self {
Self::Value { value, .. } => {
if !value.is::<T>() {
*self = Self::new_persisted(insert_with());
}
}
#[cfg(feature = "persistence")]
Self::Serialized { ron, .. } => {
*self = Self::new_persisted(from_ron_str::<T>(ron).unwrap_or_else(insert_with));
}
#[cfg(not(feature = "persistence"))]
Self::Serialized { .. } => {
*self = Self::new_persisted(insert_with());
}
}
match self {
Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type
Self::Serialized { .. } => unreachable!(),
}
}
pub(crate) fn get_mut_persisted<T: SerializableAny>(&mut self) -> Option<&mut T> {
match self {
Self::Value { value, .. } => value.downcast_mut(),
#[cfg(feature = "persistence")]
Self::Serialized { ron, .. } => {
*self = Self::new_persisted(from_ron_str::<T>(ron)?);
match self {
Self::Value { value, .. } => value.downcast_mut(),
Self::Serialized { .. } => unreachable!(),
}
}
#[cfg(not(feature = "persistence"))]
Self::Serialized { .. } => None,
}
}
#[cfg(feature = "persistence")]
fn to_serialize(&self) -> Option<SerializedElement> {
match self {
Self::Value {
value,
serialize_fn,
..
} => {
if let Some(serialize_fn) = serialize_fn {
let ron = serialize_fn(value)?;
Some(SerializedElement {
type_id: (**value).type_id().into(),
ron: ron.into(),
})
} else {
None
}
}
Self::Serialized { type_id, ron } => Some(SerializedElement {
type_id: *type_id,
ron: ron.clone(),
}),
}
}
}
#[cfg(feature = "persistence")]
fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
match ron::from_str::<T>(ron) {
Ok(value) => Some(value),
Err(_err) => {
#[cfg(feature = "tracing")]
tracing::warn!(
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
std::any::type_name::<T>(),
_err,
ron
);
None
}
}
}
// -----------------------------------------------------------------------------------------------
use crate::Id;
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value.
///
/// In other words, it maps `(Id, TypeId)` to any value you want.
///
/// Values are cloned when read, so keep them small and light.
/// If you want to store something bigger, wrap them in `Arc<Mutex<…>>`.
///
/// Values can either be "persisted" (serializable) or "temporary" (cleared when egui is shut down).
///
/// You can store state using the key [`Id::null`]. The state will then only be identified by its type.
///
/// ```
/// # use egui::{Id, util::IdTypeMap};
/// let a = Id::new("a");
/// let b = Id::new("b");
/// let mut map: IdTypeMap = Default::default();
///
/// // `a` associated with an f64 and an i32
/// map.insert_persisted(a, 3.14);
/// map.insert_temp(a, 42);
///
/// // `b` associated with an f64 and a `&'static str`
/// map.insert_persisted(b, 13.37);
/// map.insert_temp(b, "Hello World".to_owned());
///
/// // we can retrieve all four values:
/// assert_eq!(map.get_temp::<f64>(a), Some(3.14));
/// assert_eq!(map.get_temp::<i32>(a), Some(42));
/// assert_eq!(map.get_temp::<f64>(b), Some(13.37));
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
///
/// // we can retrieve them like so also:
/// assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
/// assert_eq!(map.get_persisted::<i32>(a), Some(42));
/// assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
/// ```
#[derive(Clone, Debug, Default)]
// We store use `id XOR typeid` as a key, so we don't need to hash again!
pub struct IdTypeMap(nohash_hasher::IntMap<u64, Element>);
impl IdTypeMap {
/// Insert a value that will not be persisted.
#[inline]
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.0.insert(hash, Element::new_temp(value));
}
/// Insert a value that will be persisted next time you start the app.
#[inline]
pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.0.insert(hash, Element::new_persisted(value));
}
/// Read a value without trying to deserialize a persisted value.
///
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_temp<T: 'static + Clone>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
self.0
.get_mut(&hash)
.and_then(|x| x.get_mut_temp())
.cloned()
}
/// Read a value, optionally deserializing it if available.
///
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
self.0
.get_mut(&hash)
.and_then(|x| x.get_mut_persisted())
.cloned()
}
#[inline]
pub fn get_temp_mut_or<T: 'static + Any + Clone + Send + Sync>(
&mut self,
id: Id,
or_insert: T,
) -> &mut T {
self.get_temp_mut_or_insert_with(id, || or_insert)
}
#[inline]
pub fn get_persisted_mut_or<T: SerializableAny>(&mut self, id: Id, or_insert: T) -> &mut T {
self.get_persisted_mut_or_insert_with(id, || or_insert)
}
#[inline]
pub fn get_temp_mut_or_default<T: 'static + Any + Clone + Send + Sync + Default>(
&mut self,
id: Id,
) -> &mut T {
self.get_temp_mut_or_insert_with(id, Default::default)
}
#[inline]
pub fn get_persisted_mut_or_default<T: SerializableAny + Default>(&mut self, id: Id) -> &mut T {
self.get_persisted_mut_or_insert_with(id, Default::default)
}
pub fn get_temp_mut_or_insert_with<T: 'static + Any + Clone + Send + Sync>(
&mut self,
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
use std::collections::hash_map::Entry;
match self.0.entry(hash) {
Entry::Vacant(vacant) => vacant
.insert(Element::new_temp(insert_with()))
.get_mut_temp()
.unwrap(), // this unwrap will never panic, because we insert correct type right now
Entry::Occupied(occupied) => {
occupied.into_mut().get_temp_mut_or_insert_with(insert_with)
}
}
}
pub fn get_persisted_mut_or_insert_with<T: SerializableAny>(
&mut self,
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
use std::collections::hash_map::Entry;
match self.0.entry(hash) {
Entry::Vacant(vacant) => vacant
.insert(Element::new_persisted(insert_with()))
.get_mut_persisted()
.unwrap(), // this unwrap will never panic, because we insert correct type right now
Entry::Occupied(occupied) => occupied
.into_mut()
.get_persisted_mut_or_insert_with(insert_with),
}
}
/// Remove the state of this type an id.
#[inline]
pub fn remove<T: 'static>(&mut self, id: Id) {
let hash = hash(TypeId::of::<T>(), id);
self.0.remove(&hash);
}
/// Note all state of the given type.
pub fn remove_by_type<T: 'static>(&mut self) {
let key = TypeId::of::<T>();
self.0.retain(|_, e| {
let e: &Element = e;
e.type_id() != key
});
}
#[inline]
pub fn clear(&mut self) {
self.0.clear();
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&self) -> usize {
self.0
.values()
.filter(|e| matches!(e, Element::Serialized { .. }))
.count()
}
/// Count the number of values are stored with the given type.
pub fn count<T: 'static>(&self) -> usize {
let key = TypeId::of::<T>();
self.0
.iter()
.filter(|(_, e)| {
let e: &Element = e;
e.type_id() == key
})
.count()
}
}
#[inline(always)]
fn hash(type_id: TypeId, id: Id) -> u64 {
type_id.value() ^ id.value()
}
// ----------------------------------------------------------------------------
/// How [`IdTypeMap`] is persisted.
#[cfg(feature = "persistence")]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
struct PersistedMap(Vec<(u64, SerializedElement)>);
#[cfg(feature = "persistence")]
impl PersistedMap {
fn from_map(map: &IdTypeMap) -> Self {
// filter out the elements which cannot be serialized:
Self(
map.0
.iter()
.filter_map(|(&hash, element)| Some((hash, element.to_serialize()?)))
.collect(),
)
}
fn into_map(self) -> IdTypeMap {
IdTypeMap(
self.0
.into_iter()
.map(|(hash, SerializedElement { type_id, ron })| {
(hash, Element::Serialized { type_id, ron })
})
.collect(),
)
}
}
#[cfg(feature = "persistence")]
impl serde::Serialize for IdTypeMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
PersistedMap::from_map(self).serialize(serializer)
}
}
#[cfg(feature = "persistence")]
impl<'de> serde::Deserialize<'de> for IdTypeMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
<PersistedMap>::deserialize(deserializer).map(PersistedMap::into_map)
}
}
// ----------------------------------------------------------------------------
#[test]
fn test_two_id_two_type() {
let a = Id::new("a");
let b = Id::new("b");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(a, 13.37);
map.insert_temp(b, 42);
assert_eq!(map.get_persisted::<f64>(a), Some(13.37));
assert_eq!(map.get_persisted::<i32>(b), Some(42));
assert_eq!(map.get_temp::<f64>(a), Some(13.37));
assert_eq!(map.get_temp::<i32>(b), Some(42));
}
#[test]
fn test_two_id_x_two_types() {
#![allow(clippy::approx_constant)]
let a = Id::new("a");
let b = Id::new("b");
let mut map: IdTypeMap = Default::default();
// `a` associated with an f64 and an i32
map.insert_persisted(a, 3.14);
map.insert_temp(a, 42);
// `b` associated with an f64 and a `&'static str`
map.insert_persisted(b, 13.37);
map.insert_temp(b, "Hello World".to_owned());
// we can retrieve all four values:
assert_eq!(map.get_temp::<f64>(a), Some(3.14));
assert_eq!(map.get_temp::<i32>(a), Some(42));
assert_eq!(map.get_temp::<f64>(b), Some(13.37));
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
// we can retrieve them like so also:
assert_eq!(map.get_persisted::<f64>(a), Some(3.14));
assert_eq!(map.get_persisted::<i32>(a), Some(42));
assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
}
#[test]
fn test_one_id_two_types() {
let id = Id::new("a");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(id, 13.37);
map.insert_temp(id, 42);
assert_eq!(map.get_temp::<f64>(id), Some(13.37));
assert_eq!(map.get_persisted::<f64>(id), Some(13.37));
assert_eq!(map.get_temp::<i32>(id), Some(42));
// ------------
// Test removal:
// We can remove:
map.remove::<i32>(id);
assert_eq!(map.get_temp::<i32>(id), None);
// Other type is still there, even though it is the same if:
assert_eq!(map.get_temp::<f64>(id), Some(13.37));
assert_eq!(map.get_persisted::<f64>(id), Some(13.37));
// But we can still remove the last:
map.remove::<f64>(id);
assert_eq!(map.get_temp::<f64>(id), None);
assert_eq!(map.get_persisted::<f64>(id), None);
}
#[test]
fn test_mix() {
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, PartialEq)]
struct Foo(i32);
#[derive(Clone, Debug, PartialEq)]
struct Bar(f32);
let id = Id::new("a");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(id, Foo(555));
map.insert_temp(id, Bar(1.0));
assert_eq!(map.get_temp::<Foo>(id), Some(Foo(555)));
assert_eq!(map.get_persisted::<Foo>(id), Some(Foo(555)));
assert_eq!(map.get_temp::<Bar>(id), Some(Bar(1.0)));
// ------------
// Test removal:
// We can remove:
map.remove::<Bar>(id);
assert_eq!(map.get_temp::<Bar>(id), None);
// Other type is still there, even though it is the same if:
assert_eq!(map.get_temp::<Foo>(id), Some(Foo(555)));
assert_eq!(map.get_persisted::<Foo>(id), Some(Foo(555)));
// But we can still remove the last:
map.remove::<Foo>(id);
assert_eq!(map.get_temp::<Foo>(id), None);
assert_eq!(map.get_persisted::<Foo>(id), None);
}
#[cfg(feature = "persistence")]
#[test]
fn test_mix_serialize() {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct Serializable(i32);
#[derive(Clone, Debug, PartialEq)]
struct NonSerializable(f32);
let id = Id::new("a");
let mut map: IdTypeMap = Default::default();
map.insert_persisted(id, Serializable(555));
map.insert_temp(id, NonSerializable(1.0));
assert_eq!(map.get_temp::<Serializable>(id), Some(Serializable(555)));
assert_eq!(
map.get_persisted::<Serializable>(id),
Some(Serializable(555))
);
assert_eq!(
map.get_temp::<NonSerializable>(id),
Some(NonSerializable(1.0))
);
// -----------
let serialized = ron::to_string(&map).unwrap();
// ------------
// Test removal:
// We can remove:
map.remove::<NonSerializable>(id);
assert_eq!(map.get_temp::<NonSerializable>(id), None);
// Other type is still there, even though it is the same if:
assert_eq!(map.get_temp::<Serializable>(id), Some(Serializable(555)));
assert_eq!(
map.get_persisted::<Serializable>(id),
Some(Serializable(555))
);
// But we can still remove the last:
map.remove::<Serializable>(id);
assert_eq!(map.get_temp::<Serializable>(id), None);
assert_eq!(map.get_persisted::<Serializable>(id), None);
// --------------------
// Test deserialization:
let mut map: IdTypeMap = ron::from_str(&serialized).unwrap();
assert_eq!(map.get_temp::<Serializable>(id), None);
assert_eq!(
map.get_persisted::<Serializable>(id),
Some(Serializable(555))
);
assert_eq!(map.get_temp::<Serializable>(id), Some(Serializable(555)));
}

View file

@ -1,11 +0,0 @@
//! Miscellaneous tools used by the rest of egui.
pub mod cache;
pub(crate) mod fixed_cache;
pub mod id_type_map;
pub mod undoer;
pub use id_type_map::IdTypeMap;
pub use epaint::emath::History;
pub use epaint::util::{hash, hash_with};

View file

@ -1,725 +0,0 @@
use std::borrow::Cow;
use std::sync::Arc;
use crate::{
style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2,
Style, TextStyle, Ui, Visuals,
};
/// Text and optional style choices for it.
///
/// The style choices (font, color) are applied to the entire text.
/// For more detailed control, use [`crate::text::LayoutJob`] instead.
///
/// A [`RichText`] can be used in most widgets and helper functions, e.g. [`Ui::label`] and [`Ui::button`].
///
/// ### Example
/// ```
/// use egui::{RichText, Color32};
///
/// RichText::new("Plain");
/// RichText::new("colored").color(Color32::RED);
/// RichText::new("Large and underlined").size(20.0).underline();
/// ```
#[derive(Clone, Default, PartialEq)]
pub struct RichText {
text: String,
size: Option<f32>,
family: Option<FontFamily>,
text_style: Option<TextStyle>,
background_color: Color32,
text_color: Option<Color32>,
code: bool,
strong: bool,
weak: bool,
strikethrough: bool,
underline: bool,
italics: bool,
raised: bool,
}
impl From<&str> for RichText {
#[inline]
fn from(text: &str) -> Self {
RichText::new(text)
}
}
impl From<&String> for RichText {
#[inline]
fn from(text: &String) -> Self {
RichText::new(text)
}
}
impl From<&mut String> for RichText {
#[inline]
fn from(text: &mut String) -> Self {
RichText::new(text.clone())
}
}
impl From<String> for RichText {
#[inline]
fn from(text: String) -> Self {
RichText::new(text)
}
}
impl From<Cow<'_, str>> for RichText {
#[inline]
fn from(text: Cow<'_, str>) -> Self {
Self::new(text)
}
}
impl RichText {
#[inline]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
..Default::default()
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[inline]
pub fn text(&self) -> &str {
&self.text
}
/// Select the font size (in points).
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
/// Select the font family.
///
/// This overrides the value from [`Self::text_style`].
///
/// Only the families available in [`crate::FontDefinitions::families`] may be used.
#[inline]
pub fn family(mut self, family: FontFamily) -> Self {
self.family = Some(family);
self
}
/// Select the font and size.
/// This overrides the value from [`Self::text_style`].
#[inline]
pub fn font(mut self, font_id: crate::FontId) -> Self {
let crate::FontId { size, family } = font_id;
self.size = Some(size);
self.family = Some(family);
self
}
/// Override the [`TextStyle`].
#[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style);
self
}
/// Set the [`TextStyle`] unless it has already been set
#[inline]
pub fn fallback_text_style(mut self, text_style: TextStyle) -> Self {
self.text_style.get_or_insert(text_style);
self
}
/// Use [`TextStyle::Heading`].
#[inline]
pub fn heading(self) -> Self {
self.text_style(TextStyle::Heading)
}
/// Use [`TextStyle::Monospace`].
#[inline]
pub fn monospace(self) -> Self {
self.text_style(TextStyle::Monospace)
}
/// Monospace label with different background color.
#[inline]
pub fn code(mut self) -> Self {
self.code = true;
self.text_style(TextStyle::Monospace)
}
/// Extra strong text (stronger color).
#[inline]
pub fn strong(mut self) -> Self {
self.strong = true;
self
}
/// Extra weak text (fainter color).
#[inline]
pub fn weak(mut self) -> Self {
self.weak = true;
self
}
/// Draw a line under the text.
///
/// If you want to control the line color, use [`LayoutJob`] instead.
#[inline]
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
/// Draw a line through the text, crossing it out.
///
/// If you want to control the strikethrough line color, use [`LayoutJob`] instead.
#[inline]
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
/// Tilt the characters to the right.
#[inline]
pub fn italics(mut self) -> Self {
self.italics = true;
self
}
/// Smaller text.
#[inline]
pub fn small(self) -> Self {
self.text_style(TextStyle::Small)
}
/// For e.g. exponents.
#[inline]
pub fn small_raised(self) -> Self {
self.text_style(TextStyle::Small).raised()
}
/// Align text to top. Only applicable together with [`Self::small()`].
#[inline]
pub fn raised(mut self) -> Self {
self.raised = true;
self
}
/// Fill-color behind the text.
#[inline]
pub fn background_color(mut self, background_color: impl Into<Color32>) -> Self {
self.background_color = background_color.into();
self
}
/// Override text color.
#[inline]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.text_color = Some(color.into());
self
}
/// Read the font height of the selected text style.
pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = self.size {
font_id.size = size;
}
if let Some(family) = &self.family {
font_id.family = family.clone();
}
fonts.row_height(&font_id)
}
fn into_text_job(
self,
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
let text_color = self.get_text_color(&style.visuals);
let Self {
text,
size,
family,
text_style,
background_color,
text_color: _, // already used by `get_text_color`
code,
strong: _, // already used by `get_text_color`
weak: _, // already used by `get_text_color`
strikethrough,
underline,
italics,
raised,
} = self;
let job_has_color = text_color.is_some();
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
let font_id = {
let mut font_id = text_style
.or_else(|| style.override_text_style.clone())
.map_or_else(
|| fallback_font.resolve(style),
|text_style| text_style.resolve(style),
);
if let Some(size) = size {
font_id.size = size;
}
if let Some(family) = family {
font_id.family = family;
}
font_id
};
let mut background_color = background_color;
if code {
background_color = style.visuals.code_bg_color;
}
let underline = if underline {
crate::Stroke::new(1.0, line_color)
} else {
crate::Stroke::NONE
};
let strikethrough = if strikethrough {
crate::Stroke::new(1.0, line_color)
} else {
crate::Stroke::NONE
};
let valign = if raised {
crate::Align::TOP
} else {
default_valign
};
let text_format = crate::text::TextFormat {
font_id,
color: text_color,
background: background_color,
italics,
underline,
strikethrough,
valign,
};
let job = LayoutJob::single_section(text, text_format);
WidgetTextJob { job, job_has_color }
}
fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
if let Some(text_color) = self.text_color {
Some(text_color)
} else if self.strong {
Some(visuals.strong_text_color())
} else if self.weak {
Some(visuals.weak_text_color())
} else {
visuals.override_text_color
}
}
}
// ----------------------------------------------------------------------------
/// This is how you specify text for a widget.
///
/// A lot of widgets use `impl Into<WidgetText>` as an argument,
/// allowing you to pass in [`String`], [`RichText`], [`LayoutJob`], and more.
///
/// Often a [`WidgetText`] is just a simple [`String`],
/// but it can be a [`RichText`] (text with color, style, etc),
/// a [`LayoutJob`] (for when you want full control of how the text looks)
/// or text that has already been layed out in a [`Galley`].
#[derive(Clone)]
pub enum WidgetText {
RichText(RichText),
/// Use this [`LayoutJob`] when laying out the text.
///
/// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected.
///
/// [`TextWrapping::max_width`](epaint::text::TextWrapping::max_width), [`LayoutJob::halign`], [`LayoutJob::justify`]
/// and [`LayoutJob::first_row_min_height`] will likely be determined by the [`crate::Layout`]
/// of the [`Ui`] the widget is placed in.
/// If you want all parts of the [`LayoutJob`] respected, then convert it to a
/// [`Galley`] and use [`Self::Galley`] instead.
LayoutJob(LayoutJob),
/// Use exactly this galley when painting the text.
Galley(Arc<Galley>),
}
impl Default for WidgetText {
fn default() -> Self {
Self::RichText(RichText::default())
}
}
impl WidgetText {
#[inline]
pub fn is_empty(&self) -> bool {
match self {
Self::RichText(text) => text.is_empty(),
Self::LayoutJob(job) => job.is_empty(),
Self::Galley(galley) => galley.is_empty(),
}
}
#[inline]
pub fn text(&self) -> &str {
match self {
Self::RichText(text) => text.text(),
Self::LayoutJob(job) => &job.text,
Self::Galley(galley) => galley.text(),
}
}
/// Override the [`TextStyle`] if, and only if, this is a [`RichText`].
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Set the [`TextStyle`] unless it has already been set
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Override text color if, and only if, this is a [`RichText`].
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn color(self, color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.color(color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn heading(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.heading()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn monospace(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.monospace()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn code(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.code()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn strong(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strong()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn weak(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.weak()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn underline(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.underline()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn strikethrough(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strikethrough()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn italics(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.italics()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn small(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn small_raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small_raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
/// Prefer using [`RichText`] directly!
pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.background_color(background_color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
}
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
match self {
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
Self::Galley(galley) => {
if let Some(row) = galley.rows.first() {
row.height()
} else {
galley.size().y
}
}
}
}
pub fn into_text_job(
self,
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
match self {
Self::RichText(text) => text.into_text_job(style, fallback_font, default_valign),
Self::LayoutJob(job) => WidgetTextJob {
job,
job_has_color: true,
},
Self::Galley(galley) => {
let job: LayoutJob = (*galley.job).clone();
WidgetTextJob {
job,
job_has_color: true,
}
}
}
}
/// Layout with wrap mode based on the containing [`Ui`].
///
/// wrap: override for [`Ui::wrap_text`].
pub fn into_galley(
self,
ui: &Ui,
wrap: Option<bool>,
available_width: f32,
fallback_font: impl Into<FontSelection>,
) -> WidgetTextGalley {
let wrap = wrap.unwrap_or_else(|| ui.wrap_text());
let wrap_width = if wrap { available_width } else { f32::INFINITY };
match self {
Self::RichText(text) => {
let valign = ui.layout().vertical_align();
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
text_job.job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts(|f| f.layout_job(text_job.job)),
galley_has_color: text_job.job_has_color,
}
}
Self::LayoutJob(mut job) => {
job.wrap.max_width = wrap_width;
WidgetTextGalley {
galley: ui.fonts(|f| f.layout_job(job)),
galley_has_color: true,
}
}
Self::Galley(galley) => WidgetTextGalley {
galley,
galley_has_color: true,
},
}
}
}
impl From<&str> for WidgetText {
#[inline]
fn from(text: &str) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<&String> for WidgetText {
#[inline]
fn from(text: &String) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<String> for WidgetText {
#[inline]
fn from(text: String) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<Cow<'_, str>> for WidgetText {
#[inline]
fn from(text: Cow<'_, str>) -> Self {
Self::RichText(RichText::new(text))
}
}
impl From<RichText> for WidgetText {
#[inline]
fn from(rich_text: RichText) -> Self {
Self::RichText(rich_text)
}
}
impl From<LayoutJob> for WidgetText {
#[inline]
fn from(layout_job: LayoutJob) -> Self {
Self::LayoutJob(layout_job)
}
}
impl From<Arc<Galley>> for WidgetText {
#[inline]
fn from(galley: Arc<Galley>) -> Self {
Self::Galley(galley)
}
}
// ----------------------------------------------------------------------------
#[derive(Clone, PartialEq)]
pub struct WidgetTextJob {
pub job: LayoutJob,
pub job_has_color: bool,
}
impl WidgetTextJob {
pub fn into_galley(self, fonts: &crate::text::Fonts) -> WidgetTextGalley {
let Self { job, job_has_color } = self;
let galley = fonts.layout_job(job);
WidgetTextGalley {
galley,
galley_has_color: job_has_color,
}
}
}
// ----------------------------------------------------------------------------
/// Text that has been layed out and ready to be painted.
#[derive(Clone, PartialEq)]
pub struct WidgetTextGalley {
pub galley: Arc<Galley>,
pub galley_has_color: bool,
}
impl WidgetTextGalley {
/// Size of the layed out text.
#[inline]
pub fn size(&self) -> crate::Vec2 {
self.galley.size()
}
/// Size of the layed out text.
#[inline]
pub fn text(&self) -> &str {
self.galley.text()
}
#[inline]
pub fn galley(&self) -> &Arc<Galley> {
&self.galley
}
/// Use the colors in the original [`WidgetText`] if any,
/// else fall back to the one specified by the [`WidgetVisuals`].
pub fn paint_with_visuals(
self,
painter: &crate::Painter,
text_pos: Pos2,
visuals: &WidgetVisuals,
) {
self.paint_with_fallback_color(painter, text_pos, visuals.text_color());
}
/// Use the colors in the original [`WidgetText`] if any,
/// else fall back to the given color.
pub fn paint_with_fallback_color(
self,
painter: &crate::Painter,
text_pos: Pos2,
text_color: Color32,
) {
if self.galley_has_color {
painter.galley(text_pos, self.galley);
} else {
painter.galley_with_color(text_pos, self.galley, text_color);
}
}
/// Paint with this specific color.
pub fn paint_with_color_override(
self,
painter: &crate::Painter,
text_pos: Pos2,
text_color: Color32,
) {
painter.galley_with_color(text_pos, self.galley, text_color);
}
}

View file

@ -1,571 +0,0 @@
use crate::*;
/// Clickable button with text.
///
/// See also [`Ui::button`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # fn do_stuff() {}
///
/// if ui.add(egui::Button::new("Click me")).clicked() {
/// do_stuff();
/// }
///
/// // A greyed-out and non-interactive button:
/// if ui.add_enabled(false, egui::Button::new("Can't click this")).clicked() {
/// unreachable!();
/// }
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button {
text: WidgetText,
shortcut_text: WidgetText,
wrap: Option<bool>,
/// None means default for interact
fill: Option<Color32>,
stroke: Option<Stroke>,
sense: Sense,
small: bool,
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>,
}
impl Button {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self {
text: text.into(),
shortcut_text: Default::default(),
wrap: None,
fill: None,
stroke: None,
sense: Sense::click(),
small: false,
frame: None,
min_size: Vec2::ZERO,
rounding: None,
image: None,
}
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(
texture_id: TextureId,
image_size: impl Into<Vec2>,
text: impl Into<WidgetText>,
) -> Self {
Self {
image: Some(widgets::Image::new(texture_id, image_size)),
..Self::new(text)
}
}
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
///
/// By default [`Self::wrap`] will be true in vertical layouts
/// and horizontal layouts with wrapping,
/// and false on non-wrapping horizontal layouts.
///
/// Note that any `\n` in the text will always produce a new line.
#[inline]
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap);
self
}
/// Override background fill color. Note that this will override any on-hover effects.
/// Calling this will also turn on the frame.
pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
self.fill = Some(fill.into());
self.frame = Some(true);
self
}
/// Override button stroke. Note that this will override any on-hover effects.
/// Calling this will also turn on the frame.
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = Some(stroke.into());
self.frame = Some(true);
self
}
/// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self {
self.text = self.text.text_style(TextStyle::Body);
self.small = true;
self
}
/// Turn off the frame
pub fn frame(mut self, frame: bool) -> Self {
self.frame = Some(frame);
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the minimum size of the button.
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
/// Set the rounding of the button.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = Some(rounding.into());
self
}
/// Show some text on the right side of the button, in weak color.
///
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
///
/// The text can be created with [`Context::format_shortcut`].
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
self.shortcut_text = shortcut_text.into();
self
}
}
impl Widget for Button {
fn ui(self, ui: &mut Ui) -> Response {
let Button {
text,
shortcut_text,
wrap,
fill,
stroke,
sense,
small,
frame,
min_size,
rounding,
image,
} = self;
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = ui.spacing().button_padding;
if small {
button_padding.y = 0.0;
}
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if let Some(image) = image {
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
}
if !shortcut_text.is_empty() {
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
}
let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button);
let shortcut_text = (!shortcut_text.is_empty())
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
let mut desired_size = text.size();
if let Some(image) = image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y);
}
if let Some(shortcut_text) = &shortcut_text {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
desired_size.y = desired_size.y.max(shortcut_text.size().y);
}
desired_size += 2.0 * button_padding;
if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
}
desired_size = desired_size.at_least(min_size);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let rounding = rounding.unwrap_or(visuals.rounding);
ui.painter()
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if let Some(image) = image {
let icon_spacing = ui.spacing().icon_spacing;
pos2(
rect.min.x + button_padding.x + image.size().x + icon_spacing,
rect.center().y - 0.5 * text.size().y,
)
} else {
ui.layout()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
};
text.paint_with_visuals(ui.painter(), text_pos, visuals);
if let Some(shortcut_text) = shortcut_text {
let shortcut_text_pos = pos2(
rect.max.x - button_padding.x - shortcut_text.size().x,
rect.center().y - 0.5 * shortcut_text.size().y,
);
shortcut_text.paint_with_fallback_color(
ui.painter(),
shortcut_text_pos,
ui.visuals().weak_text_color(),
);
}
if let Some(image) = image {
let image_rect = Rect::from_min_size(
pos2(
rect.min.x + button_padding.x,
rect.center().y - 0.5 - (image.size().y / 2.0),
),
image.size(),
);
image.paint_at(ui, image_rect);
}
}
response
}
}
// ----------------------------------------------------------------------------
// TODO(emilk): allow checkbox without a text label
/// Boolean on/off control with text label.
///
/// Usually you'd use [`Ui::checkbox`] instead.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_bool = true;
/// // These are equivalent:
/// ui.checkbox(&mut my_bool, "Checked");
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Checkbox<'a> {
checked: &'a mut bool,
text: WidgetText,
}
impl<'a> Checkbox<'a> {
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
Checkbox {
checked,
text: text.into(),
}
}
pub fn without_text(checked: &'a mut bool) -> Self {
Self::new(checked, WidgetText::default())
}
}
impl<'a> Widget for Checkbox<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let Checkbox { checked, text } = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
let icon_spacing = spacing.icon_spacing;
let (text, mut desired_size) = if text.is_empty() {
(None, vec2(icon_width, 0.0))
} else {
let total_extra = vec2(icon_width + icon_spacing, 0.0);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
let mut desired_size = total_extra + text.size();
desired_size = desired_size.at_least(spacing.interact_size);
(Some(text), desired_size)
};
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
desired_size.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked() {
*checked = !*checked;
response.mark_changed();
}
response.widget_info(|| {
WidgetInfo::selected(
WidgetType::Checkbox,
*checked,
text.as_ref().map_or("", |x| x.text()),
)
});
if ui.is_rect_visible(rect) {
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response);
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape {
rect: big_icon_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
if *checked {
// Check mark:
ui.painter().add(Shape::line(
vec![
pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
pos2(small_icon_rect.right(), small_icon_rect.top()),
],
visuals.fg_stroke,
));
}
if let Some(text) = text {
let text_pos = pos2(
rect.min.x + icon_width + icon_spacing,
rect.center().y - 0.5 * text.size().y,
);
text.paint_with_visuals(ui.painter(), text_pos, visuals);
}
}
response
}
}
// ----------------------------------------------------------------------------
/// One out of several alternatives, either selected or not.
///
/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// #[derive(PartialEq)]
/// enum Enum { First, Second, Third }
/// let mut my_enum = Enum::First;
///
/// ui.radio_value(&mut my_enum, Enum::First, "First");
///
/// // is equivalent to:
///
/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
/// my_enum = Enum::First
/// }
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct RadioButton {
checked: bool,
text: WidgetText,
}
impl RadioButton {
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
Self {
checked,
text: text.into(),
}
}
}
impl Widget for RadioButton {
fn ui(self, ui: &mut Ui) -> Response {
let RadioButton { checked, text } = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
let icon_spacing = spacing.icon_spacing;
let (text, mut desired_size) = if text.is_empty() {
(None, vec2(icon_width, 0.0))
} else {
let total_extra = vec2(icon_width + icon_spacing, 0.0);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
let mut desired_size = total_extra + text.size();
desired_size = desired_size.at_least(spacing.interact_size);
(Some(text), desired_size)
};
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
response.widget_info(|| {
WidgetInfo::selected(
WidgetType::RadioButton,
checked,
text.as_ref().map_or("", |x| x.text()),
)
});
if ui.is_rect_visible(rect) {
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
let visuals = ui.style().interact(&response);
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
let painter = ui.painter();
painter.add(epaint::CircleShape {
center: big_icon_rect.center(),
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
if checked {
painter.add(epaint::CircleShape {
center: small_icon_rect.center(),
radius: small_icon_rect.width() / 3.0,
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
// fill: ui.visuals().selection.stroke.color, // too much color
stroke: Default::default(),
});
}
if let Some(text) = text {
let text_pos = pos2(
rect.min.x + icon_width + icon_spacing,
rect.center().y - 0.5 * text.size().y,
);
text.paint_with_visuals(ui.painter(), text_pos, visuals);
}
}
response
}
}
// ----------------------------------------------------------------------------
/// A clickable image within a frame.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Debug)]
pub struct ImageButton {
image: widgets::Image,
sense: Sense,
frame: bool,
selected: bool,
}
impl ImageButton {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self {
image: widgets::Image::new(texture_id, size),
sense: Sense::click(),
frame: true,
selected: false,
}
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.image = self.image.uv(uv);
self
}
/// Multiply image color with this. Default is WHITE (no tint).
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.image = self.image.tint(tint);
self
}
/// If `true`, mark this button as "selected".
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
/// Turn off the frame
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
}
impl Widget for ImageButton {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
image,
sense,
frame,
selected,
} = self;
let padding = if frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
} else {
Vec2::ZERO
};
let padded_size = image.size() + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, sense);
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
Rounding::none(),
selection.bg_fill,
selection.stroke,
)
} else if frame {
let visuals = ui.style().interact(&response);
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
Default::default()
};
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
.align_size_within_rect(image.size(), rect.shrink2(padding));
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
image.paint_at(ui, image_rect);
// Draw frame outline:
ui.painter()
.rect_stroke(rect.expand2(expansion), rounding, stroke);
}
response
}
}

View file

@ -1,440 +0,0 @@
//! Color picker widgets.
use crate::util::fixed_cache::FixedCache;
use crate::*;
use epaint::{ecolor::*, *};
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
if color.into().intensity() < 0.5 {
Color32::WHITE
} else {
Color32::BLACK
}
}
/// Number of vertices per dimension in the color sliders.
/// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 6;
fn background_checkers(painter: &Painter, rect: Rect) {
let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides
if !rect.is_positive() {
return;
}
let dark_color = Color32::from_gray(32);
let bright_color = Color32::from_gray(128);
let checker_size = Vec2::splat(rect.height() / 2.0);
let n = (rect.width() / checker_size.x).round() as u32;
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, dark_color);
let mut top = true;
for i in 0..n {
let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
let small_rect = if top {
Rect::from_min_size(pos2(x, rect.top()), checker_size)
} else {
Rect::from_min_size(pos2(x, rect.center().y), checker_size)
};
mesh.add_colored_rect(small_rect, bright_color);
top = !top;
}
painter.add(Shape::mesh(mesh));
}
/// Show a color with background checkers to demonstrate transparency (if any).
pub fn show_color(ui: &mut Ui, color: impl Into<Color32>, desired_size: Vec2) -> Response {
show_color32(ui, color.into(), desired_size)
}
fn show_color32(ui: &mut Ui, color: Color32, desired_size: Vec2) -> Response {
let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
if ui.is_rect_visible(rect) {
show_color_at(ui.painter(), color, rect);
}
response
}
/// Show a color with background checkers to demonstrate transparency (if any).
pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
if color.is_opaque() {
painter.rect_filled(rect, 0.0, color);
} else {
// Transparent: how both the transparent and opaque versions of the color
background_checkers(painter, rect);
if color == Color32::TRANSPARENT {
// There is no opaque version, so just show the background checkers
} else {
let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
painter.rect_filled(left, 0.0, color);
painter.rect_filled(right, 0.0, color.to_opaque());
}
}
}
fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
let size = ui.spacing().interact_size;
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));
if ui.is_rect_visible(rect) {
let visuals = if open {
&ui.visuals().widgets.open
} else {
ui.style().interact(&response)
};
let rect = rect.expand(visuals.expansion);
show_color_at(ui.painter(), color, rect);
let rounding = visuals.rounding.at_most(2.0);
ui.painter()
.rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border
}
response
}
fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
#![allow(clippy::identity_op)]
let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if let Some(mpos) = response.interact_pointer_pos() {
*value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
}
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
background_checkers(ui.painter(), rect); // for alpha:
{
// fill color:
let mut mesh = Mesh::default();
for i in 0..=N {
let t = i as f32 / (N as f32);
let color = color_at(t);
let x = lerp(rect.left()..=rect.right(), t);
mesh.colored_vertex(pos2(x, rect.top()), color);
mesh.colored_vertex(pos2(x, rect.bottom()), color);
if i < N {
mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
}
}
ui.painter().add(Shape::mesh(mesh));
}
ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline
{
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *value);
let r = rect.height() / 4.0;
let picked_color = color_at(*value);
ui.painter().add(Shape::convex_polygon(
vec![
pos2(x, rect.center().y), // tip
pos2(x + r, rect.bottom()), // right bottom
pos2(x - r, rect.bottom()), // left bottom
],
picked_color,
Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
));
}
}
response
}
fn color_slider_2d(
ui: &mut Ui,
x_value: &mut f32,
y_value: &mut f32,
color_at: impl Fn(f32, f32) -> Color32,
) -> Response {
let desired_size = Vec2::splat(ui.spacing().slider_width);
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
if let Some(mpos) = response.interact_pointer_pos() {
*x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
*y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
}
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let mut mesh = Mesh::default();
for xi in 0..=N {
for yi in 0..=N {
let xt = xi as f32 / (N as f32);
let yt = yi as f32 / (N as f32);
let color = color_at(xt, yt);
let x = lerp(rect.left()..=rect.right(), xt);
let y = lerp(rect.bottom()..=rect.top(), yt);
mesh.colored_vertex(pos2(x, y), color);
if xi < N && yi < N {
let x_offset = 1;
let y_offset = N + 1;
let tl = yi * y_offset + xi;
mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
}
}
}
ui.painter().add(Shape::mesh(mesh)); // fill
ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *x_value);
let y = lerp(rect.bottom()..=rect.top(), *y_value);
let picked_color = color_at(*x_value, *y_value);
ui.painter().add(epaint::CircleShape {
center: pos2(x, y),
radius: rect.width() / 12.0,
fill: picked_color,
stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
});
}
response
}
/// What options to show for alpha
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Alpha {
// Set alpha to 1.0, and show no option for it.
Opaque,
// Only show normal blend options for it.
OnlyBlend,
// Show both blend and additive options.
BlendOrAdditive,
}
fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
let color = color.into();
ui.horizontal(|ui| {
let [r, g, b, a] = color.to_array();
if ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
} else {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
}
}
if alpha == Alpha::Opaque {
ui.label(format!("rgb({}, {}, {})", r, g, b))
.on_hover_text("Red Green Blue");
} else {
ui.label(format!("rgba({}, {}, {}, {})", r, g, b, a))
.on_hover_text("Red Green Blue with premultiplied Alpha");
}
});
}
fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) {
let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
show_color(ui, *hsva, current_color_size).on_hover_text("Selected color");
color_text_ui(ui, *hsva, alpha);
if alpha == Alpha::BlendOrAdditive {
// We signal additive blending by storing a negative alpha (a bit ironic).
let a = &mut hsva.a;
let mut additive = *a < 0.0;
ui.horizontal(|ui| {
ui.label("Blending:");
ui.radio_value(&mut additive, false, "Normal");
ui.radio_value(&mut additive, true, "Additive");
if additive {
*a = -a.abs();
}
if !additive {
*a = a.abs();
}
});
}
let additive = hsva.a < 0.0;
let opaque = HsvaGamma { a: 1.0, ..*hsva };
if alpha == Alpha::Opaque {
hsva.a = 1.0;
} else {
let a = &mut hsva.a;
if alpha == Alpha::OnlyBlend {
if *a < 0.0 {
*a = 0.5; // was additive, but isn't allowed to be
}
color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
} else if !additive {
color_slider_1d(ui, a, |a| HsvaGamma { a, ..opaque }.into()).on_hover_text("Alpha");
}
}
let HsvaGamma { h, s, v, a: _ } = hsva;
color_slider_1d(ui, h, |h| {
HsvaGamma {
h,
s: 1.0,
v: 1.0,
a: 1.0,
}
.into()
})
.on_hover_text("Hue");
if false {
color_slider_1d(ui, s, |s| HsvaGamma { s, ..opaque }.into()).on_hover_text("Saturation");
}
if false {
color_slider_1d(ui, v, |v| HsvaGamma { v, ..opaque }.into()).on_hover_text("Value");
}
color_slider_2d(ui, v, s, |v, s| HsvaGamma { s, v, ..opaque }.into());
}
//// Shows a color picker where the user can change the given [`Hsva`] color.
///
/// Returns `true` on change.
pub fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool {
let mut hsvag = HsvaGamma::from(*hsva);
ui.vertical(|ui| {
color_picker_hsvag_2d(ui, &mut hsvag, alpha);
});
let new_hasva = Hsva::from(hsvag);
if *hsva == new_hasva {
false
} else {
*hsva = new_hasva;
true
}
}
/// Shows a color picker where the user can change the given [`Color32`] color.
///
/// Returns `true` on change.
pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> bool {
let mut hsva = color_cache_get(ui.ctx(), *srgba);
let changed = color_picker_hsva_2d(ui, &mut hsva, alpha);
*srgba = Color32::from(hsva);
color_cache_set(ui.ctx(), *srgba, hsva);
changed
}
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let popup_id = ui.auto_id_with("popup");
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");
}
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
const COLOR_SLIDER_WIDTH: f32 = 210.0;
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
let area_response = Area::new(popup_id)
.order(Order::Foreground)
.fixed_pos(button_response.rect.max)
.constrain(true)
.show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
Frame::popup(ui.style()).show(ui, |ui| {
if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed();
}
});
})
.response;
if !button_response.clicked()
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
ui.memory_mut(|mem| mem.close_popup());
}
}
button_response
}
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_srgba(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> Response {
let mut hsva = color_cache_get(ui.ctx(), *srgba);
let response = color_edit_button_hsva(ui, &mut hsva, alpha);
*srgba = Color32::from(hsva);
color_cache_set(ui.ctx(), *srgba, hsva);
response
}
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
/// The given color is in `sRGB` space.
pub fn color_edit_button_srgb(ui: &mut Ui, srgb: &mut [u8; 3]) -> Response {
let mut srgba = Color32::from_rgb(srgb[0], srgb[1], srgb[2]);
let response = color_edit_button_srgba(ui, &mut srgba, Alpha::Opaque);
srgb[0] = srgba[0];
srgb[1] = srgba[1];
srgb[2] = srgba[2];
response
}
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_rgba(ui: &mut Ui, rgba: &mut Rgba, alpha: Alpha) -> Response {
let mut hsva = color_cache_get(ui.ctx(), *rgba);
let response = color_edit_button_hsva(ui, &mut hsva, alpha);
*rgba = Rgba::from(hsva);
color_cache_set(ui.ctx(), *rgba, hsva);
response
}
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button_rgb(ui: &mut Ui, rgb: &mut [f32; 3]) -> Response {
let mut rgba = Rgba::from_rgb(rgb[0], rgb[1], rgb[2]);
let response = color_edit_button_rgba(ui, &mut rgba, Alpha::Opaque);
rgb[0] = rgba[0];
rgb[1] = rgba[1];
rgb[2] = rgba[2];
response
}
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
let rgba = rgba.into();
use_color_cache(ctx, |cc| cc.get(&rgba).copied()).unwrap_or_else(|| Hsva::from(rgba))
}
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
let rgba = rgba.into();
use_color_cache(ctx, |cc| cc.set(rgba, hsva));
}
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
}

View file

@ -1,662 +0,0 @@
#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString`
use std::{cmp::Ordering, ops::RangeInclusive};
use crate::*;
// ----------------------------------------------------------------------------
/// Same state for all [`DragValue`]s.
#[derive(Clone, Debug, Default)]
pub(crate) struct MonoState {
last_dragged_id: Option<Id>,
last_dragged_value: Option<f64>,
/// For temporary edit of a [`DragValue`] value.
/// Couples with the current focus id.
edit_string: Option<String>,
}
impl MonoState {
pub(crate) fn end_frame(&mut self, input: &InputState) {
if input.pointer.any_pressed() || input.pointer.any_released() {
self.last_dragged_id = None;
self.last_dragged_value = None;
}
}
}
// ----------------------------------------------------------------------------
type NumFormatter<'a> = Box<dyn 'a + Fn(f64, RangeInclusive<usize>) -> String>;
type NumParser<'a> = Box<dyn 'a + Fn(&str) -> Option<f64>>;
// ----------------------------------------------------------------------------
/// Combined into one function (rather than two) to make it easier
/// for the borrow checker.
type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>;
fn get(get_set_value: &mut GetSetValue<'_>) -> f64 {
(get_set_value)(None)
}
fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
(get_set_value)(Some(value));
}
/// A numeric value that you can change by dragging the number. More compact than a [`Slider`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_f32: f32 = 0.0;
/// ui.add(egui::DragValue::new(&mut my_f32).speed(0.1));
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct DragValue<'a> {
get_set_value: GetSetValue<'a>,
speed: f64,
prefix: String,
suffix: String,
clamp_range: RangeInclusive<f64>,
min_decimals: usize,
max_decimals: Option<usize>,
custom_formatter: Option<NumFormatter<'a>>,
custom_parser: Option<NumParser<'a>>,
}
impl<'a> DragValue<'a> {
pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
let slf = Self::from_get_set(move |v: Option<f64>| {
if let Some(v) = v {
*value = Num::from_f64(v);
}
value.to_f64()
});
if Num::INTEGRAL {
slf.max_decimals(0)
.clamp_range(Num::MIN..=Num::MAX)
.speed(0.25)
} else {
slf
}
}
pub fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> f64) -> Self {
Self {
get_set_value: Box::new(get_set_value),
speed: 1.0,
prefix: Default::default(),
suffix: Default::default(),
clamp_range: f64::NEG_INFINITY..=f64::INFINITY,
min_decimals: 0,
max_decimals: None,
custom_formatter: None,
custom_parser: None,
}
}
/// How much the value changes when dragged one point (logical pixel).
pub fn speed(mut self, speed: impl Into<f64>) -> Self {
self.speed = speed.into();
self
}
/// Clamp incoming and outgoing values to this range.
pub fn clamp_range<Num: emath::Numeric>(mut self, clamp_range: RangeInclusive<Num>) -> Self {
self.clamp_range = clamp_range.start().to_f64()..=clamp_range.end().to_f64();
self
}
/// Show a prefix before the number, e.g. "x: "
pub fn prefix(mut self, prefix: impl ToString) -> Self {
self.prefix = prefix.to_string();
self
}
/// Add a suffix to the number, this can be e.g. a unit ("°" or " m")
pub fn suffix(mut self, suffix: impl ToString) -> Self {
self.suffix = suffix.to_string();
self
}
// TODO(emilk): we should also have a "min precision".
/// Set a minimum number of decimals to display.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
self.min_decimals = min_decimals;
self
}
// TODO(emilk): we should also have a "max precision".
/// Set a maximum number of decimals to display.
/// Values will also be rounded to this number of decimals.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
pub fn max_decimals(mut self, max_decimals: usize) -> Self {
self.max_decimals = Some(max_decimals);
self
}
pub fn max_decimals_opt(mut self, max_decimals: Option<usize>) -> Self {
self.max_decimals = max_decimals;
self
}
/// Set an exact number of decimals to display.
/// Values will also be rounded to this number of decimals.
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
pub fn fixed_decimals(mut self, num_decimals: usize) -> Self {
self.min_decimals = num_decimals;
self.max_decimals = Some(num_decimals);
self
}
/// Set custom formatter defining how numbers are converted into text.
///
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
/// the decimal range i.e. minimum and maximum number of decimal places shown.
///
/// See also: [`DragValue::custom_parser`]
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_i32: i32 = 0;
/// ui.add(egui::DragValue::new(&mut my_i32)
/// .clamp_range(0..=((60 * 60 * 24) - 1))
/// .custom_formatter(|n, _| {
/// let n = n as i32;
/// let hours = n / (60 * 60);
/// let mins = (n / 60) % 60;
/// let secs = n % 60;
/// format!("{hours:02}:{mins:02}:{secs:02}")
/// })
/// .custom_parser(|s| {
/// let parts: Vec<&str> = s.split(':').collect();
/// if parts.len() == 3 {
/// parts[0].parse::<i32>().and_then(|h| {
/// parts[1].parse::<i32>().and_then(|m| {
/// parts[2].parse::<i32>().map(|s| {
/// ((h * 60 * 60) + (m * 60) + s) as f64
/// })
/// })
/// })
/// .ok()
/// } else {
/// None
/// }
/// }));
/// # });
/// ```
pub fn custom_formatter(
mut self,
formatter: impl 'a + Fn(f64, RangeInclusive<usize>) -> String,
) -> Self {
self.custom_formatter = Some(Box::new(formatter));
self
}
/// Set custom parser defining how the text input is parsed into a number.
///
/// A custom parser takes an `&str` to parse into a number and returns a `f64` if it was successfully parsed
/// or `None` otherwise.
///
/// See also: [`DragValue::custom_formatter`]
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_i32: i32 = 0;
/// ui.add(egui::DragValue::new(&mut my_i32)
/// .clamp_range(0..=((60 * 60 * 24) - 1))
/// .custom_formatter(|n, _| {
/// let n = n as i32;
/// let hours = n / (60 * 60);
/// let mins = (n / 60) % 60;
/// let secs = n % 60;
/// format!("{hours:02}:{mins:02}:{secs:02}")
/// })
/// .custom_parser(|s| {
/// let parts: Vec<&str> = s.split(':').collect();
/// if parts.len() == 3 {
/// parts[0].parse::<i32>().and_then(|h| {
/// parts[1].parse::<i32>().and_then(|m| {
/// parts[2].parse::<i32>().map(|s| {
/// ((h * 60 * 60) + (m * 60) + s) as f64
/// })
/// })
/// })
/// .ok()
/// } else {
/// None
/// }
/// }));
/// # });
/// ```
pub fn custom_parser(mut self, parser: impl 'a + Fn(&str) -> Option<f64>) -> Self {
self.custom_parser = Some(Box::new(parser));
self
}
/// Set `custom_formatter` and `custom_parser` to display and parse numbers as binary integers. Floating point
/// numbers are *not* supported.
///
/// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
/// prefixed with additional 0s to match `min_width`.
///
/// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
/// they will be prefixed with a '-' sign.
///
/// # Panics
///
/// Panics if `min_width` is 0.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_i32: i32 = 0;
/// ui.add(egui::DragValue::new(&mut my_i32).binary(64, false));
/// # });
/// ```
pub fn binary(self, min_width: usize, twos_complement: bool) -> Self {
assert!(
min_width > 0,
"DragValue::binary: `min_width` must be greater than 0"
);
if twos_complement {
self.custom_formatter(move |n, _| format!("{:0>min_width$b}", n as i64))
} else {
self.custom_formatter(move |n, _| {
let sign = if n < 0.0 { "-" } else { "" };
format!("{sign}{:0>min_width$b}", n.abs() as i64)
})
}
.custom_parser(|s| i64::from_str_radix(s, 2).map(|n| n as f64).ok())
}
/// Set `custom_formatter` and `custom_parser` to display and parse numbers as octal integers. Floating point
/// numbers are *not* supported.
///
/// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
/// prefixed with additional 0s to match `min_width`.
///
/// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
/// they will be prefixed with a '-' sign.
///
/// # Panics
///
/// Panics if `min_width` is 0.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_i32: i32 = 0;
/// ui.add(egui::DragValue::new(&mut my_i32).octal(22, false));
/// # });
/// ```
pub fn octal(self, min_width: usize, twos_complement: bool) -> Self {
assert!(
min_width > 0,
"DragValue::octal: `min_width` must be greater than 0"
);
if twos_complement {
self.custom_formatter(move |n, _| format!("{:0>min_width$o}", n as i64))
} else {
self.custom_formatter(move |n, _| {
let sign = if n < 0.0 { "-" } else { "" };
format!("{sign}{:0>min_width$o}", n.abs() as i64)
})
}
.custom_parser(|s| i64::from_str_radix(s, 8).map(|n| n as f64).ok())
}
/// Set `custom_formatter` and `custom_parser` to display and parse numbers as hexadecimal integers. Floating point
/// numbers are *not* supported.
///
/// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
/// prefixed with additional 0s to match `min_width`.
///
/// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
/// they will be prefixed with a '-' sign.
///
/// # Panics
///
/// Panics if `min_width` is 0.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut my_i32: i32 = 0;
/// ui.add(egui::DragValue::new(&mut my_i32).hexadecimal(16, false, true));
/// # });
/// ```
pub fn hexadecimal(self, min_width: usize, twos_complement: bool, upper: bool) -> Self {
assert!(
min_width > 0,
"DragValue::hexadecimal: `min_width` must be greater than 0"
);
match (twos_complement, upper) {
(true, true) => {
self.custom_formatter(move |n, _| format!("{:0>min_width$X}", n as i64))
}
(true, false) => {
self.custom_formatter(move |n, _| format!("{:0>min_width$x}", n as i64))
}
(false, true) => self.custom_formatter(move |n, _| {
let sign = if n < 0.0 { "-" } else { "" };
format!("{sign}{:0>min_width$X}", n.abs() as i64)
}),
(false, false) => self.custom_formatter(move |n, _| {
let sign = if n < 0.0 { "-" } else { "" };
format!("{sign}{:0>min_width$x}", n.abs() as i64)
}),
}
.custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok())
}
}
impl<'a> Widget for DragValue<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
mut get_set_value,
speed,
clamp_range,
prefix,
suffix,
min_decimals,
max_decimals,
custom_formatter,
custom_parser,
} = self;
let shift = ui.input(|i| i.modifiers.shift_only());
// The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id();
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
// The following ensures that when a `DragValue` receives focus,
// it is immediately rendered in edit mode, rather than being rendered
// in button mode for just one frame. This is important for
// screen readers.
let is_kb_editing = ui.memory_mut(|mem| {
mem.interested_in_focus(id);
let is_kb_editing = mem.has_focus(id);
if mem.gained_focus(id) {
mem.drag_value.edit_string = None;
}
is_kb_editing
});
let old_value = get(&mut get_set_value);
let mut value = old_value;
let aim_rad = ui.input(|i| i.aim_radius() as f64);
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
let auto_decimals = auto_decimals + is_slow_speed as usize;
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
let change = ui.input_mut(|input| {
let mut change = 0.0;
if is_kb_editing {
// This deliberately doesn't listen for left and right arrow keys,
// because when editing, these are used to move the caret.
// This behavior is consistent with other editable spinner/stepper
// implementations, such as Chromium's (for HTML5 number input).
// It is also normal for such controls to go directly into edit mode
// when they receive keyboard focus, and some screen readers
// assume this behavior, so having a separate mode for incrementing
// and decrementing, that supports all arrow keys, would be
// problematic.
change += input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
}
#[cfg(feature = "accesskit")]
{
use accesskit::Action;
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
}
change
});
#[cfg(feature = "accesskit")]
{
use accesskit::{Action, ActionData};
ui.input(|input| {
for request in input.accesskit_action_requests(id, Action::SetValue) {
if let Some(ActionData::NumericValue(new_value)) = request.data {
value = new_value;
}
}
});
}
if change != 0.0 {
value += speed * change;
value = emath::round_to_decimals(value, auto_decimals);
}
value = clamp_to_range(value, clamp_range.clone());
if old_value != value {
set(&mut get_set_value, value);
ui.memory_mut(|mem| mem.drag_value.edit_string = None);
}
let value_text = match custom_formatter {
Some(custom_formatter) => custom_formatter(value, auto_decimals..=max_decimals),
None => {
if value == 0.0 {
"0".to_owned()
} else {
emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals)
}
}
};
let text_style = ui.style().drag_value_text_style.clone();
// some clones below are redundant if AccessKit is disabled
#[allow(clippy::redundant_clone)]
let mut response = if is_kb_editing {
let mut value_text = ui
.memory_mut(|mem| mem.drag_value.edit_string.take())
.unwrap_or_else(|| value_text.clone());
let response = ui.add(
TextEdit::singleline(&mut value_text)
.clip_text(false)
.horizontal_align(ui.layout().horizontal_align())
.vertical_align(ui.layout().vertical_align())
.margin(ui.spacing().button_padding)
.min_size(ui.spacing().interact_size)
.id(id)
.desired_width(ui.spacing().interact_size.x)
.font(text_style),
);
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
// See https://github.com/emilk/egui/issues/2687
if response.lost_focus() {
let parsed_value = match custom_parser {
Some(parser) => parser(&value_text),
None => value_text.parse().ok(),
};
if let Some(parsed_value) = parsed_value {
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
set(&mut get_set_value, parsed_value);
}
}
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text));
response
} else {
let button = Button::new(
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
.text_style(text_style),
)
.wrap(false)
.sense(Sense::click_and_drag())
.min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
let response = ui.add(button);
let mut response = response.on_hover_cursor(CursorIcon::ResizeHorizontal);
if ui.style().explanation_tooltips {
response = response .on_hover_text(format!(
"{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
prefix,
value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32
suffix
));
}
if response.clicked() {
ui.memory_mut(|mem| {
mem.drag_value.edit_string = None;
mem.request_focus(id);
});
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
state.set_ccursor_range(Some(text::CCursorRange::two(
epaint::text::cursor::CCursor::default(),
epaint::text::cursor::CCursor::new(value_text.chars().count()),
)));
state.store(ui.ctx(), response.id);
} else if response.dragged() {
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
let mdelta = response.drag_delta();
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
let speed = if is_slow_speed { speed / 10.0 } else { speed };
let delta_value = delta_points as f64 * speed;
if delta_value != 0.0 {
let mut drag_state = ui.memory_mut(|mem| std::mem::take(&mut mem.drag_value));
// Since we round the value being dragged, we need to store the full precision value in memory:
let stored_value = (drag_state.last_dragged_id == Some(response.id))
.then_some(drag_state.last_dragged_value)
.flatten();
let stored_value = stored_value.unwrap_or(value);
let stored_value = stored_value + delta_value;
let aim_delta = aim_rad * speed;
let rounded_new_value = emath::smart_aim::best_in_range_f64(
stored_value - aim_delta,
stored_value + aim_delta,
);
let rounded_new_value =
emath::round_to_decimals(rounded_new_value, auto_decimals);
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range.clone());
set(&mut get_set_value, rounded_new_value);
drag_state.last_dragged_id = Some(response.id);
drag_state.last_dragged_value = Some(stored_value);
ui.memory_mut(|mem| mem.drag_value = drag_state);
}
}
response
};
response.changed = get(&mut get_set_value) != old_value;
response.widget_info(|| WidgetInfo::drag_value(value));
#[cfg(feature = "accesskit")]
ui.ctx().accesskit_node_builder(response.id, |builder| {
use accesskit::Action;
// If either end of the range is unbounded, it's better
// to leave the corresponding AccessKit field set to None,
// to allow for platform-specific default behavior.
if clamp_range.start().is_finite() {
builder.set_min_numeric_value(*clamp_range.start());
}
if clamp_range.end().is_finite() {
builder.set_max_numeric_value(*clamp_range.end());
}
builder.set_numeric_value_step(speed);
builder.add_action(Action::SetValue);
if value < *clamp_range.end() {
builder.add_action(Action::Increment);
}
if value > *clamp_range.start() {
builder.add_action(Action::Decrement);
}
// The name field is set to the current value by the button,
// but we don't want it set that way on this widget type.
builder.clear_name();
// Always expose the value as a string. This makes the widget
// more stable to accessibility users as it switches
// between edit and button modes. This is particularly important
// for VoiceOver on macOS; if the value is not exposed as a string
// when the widget is in button mode, then VoiceOver speaks
// the value (or a percentage if the widget has a clamp range)
// when the widget loses focus, overriding the announcement
// of the newly focused widget. This is certainly a VoiceOver bug,
// but it's good to make our software work as well as possible
// with existing assistive technology. However, if the widget
// has a prefix and/or suffix, expose those when in button mode,
// just as they're exposed on the screen. This triggers the
// VoiceOver bug just described, but exposing all information
// is more important, and at least we can avoid the bug
// for instances of the widget with no prefix or suffix.
//
// The value is exposed as a string by the text edit widget
// when in edit mode.
if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
builder.set_value(value_text);
}
});
response
}
}
fn clamp_to_range(x: f64, range: RangeInclusive<f64>) -> f64 {
let (mut min, mut max) = (*range.start(), *range.end());
if min.total_cmp(&max) == Ordering::Greater {
(min, max) = (max, min);
}
match x.total_cmp(&min) {
Ordering::Less | Ordering::Equal => min,
Ordering::Greater => match x.total_cmp(&max) {
Ordering::Greater | Ordering::Equal => max,
Ordering::Less => x,
},
}
}
#[cfg(test)]
mod tests {
use super::clamp_to_range;
macro_rules! total_assert_eq {
($a:expr, $b:expr) => {
assert!(
matches!($a.total_cmp(&$b), std::cmp::Ordering::Equal),
"{} != {}",
$a,
$b
);
};
}
#[test]
fn test_total_cmp_clamp_to_range() {
total_assert_eq!(0.0_f64, clamp_to_range(-0.0, 0.0..=f64::MAX));
total_assert_eq!(-0.0_f64, clamp_to_range(0.0, -1.0..=-0.0));
total_assert_eq!(-1.0_f64, clamp_to_range(-25.0, -1.0..=1.0));
total_assert_eq!(5.0_f64, clamp_to_range(5.0, -1.0..=10.0));
total_assert_eq!(15.0_f64, clamp_to_range(25.0, -1.0..=15.0));
total_assert_eq!(1.0_f64, clamp_to_range(1.0, 1.0..=10.0));
total_assert_eq!(10.0_f64, clamp_to_range(10.0, 1.0..=10.0));
total_assert_eq!(5.0_f64, clamp_to_range(5.0, 10.0..=1.0));
total_assert_eq!(5.0_f64, clamp_to_range(15.0, 5.0..=1.0));
total_assert_eq!(1.0_f64, clamp_to_range(-5.0, 5.0..=1.0));
}
}

View file

@ -1,131 +0,0 @@
use crate::*;
/// Clickable text, that looks like a hyperlink.
///
/// To link to a web page, use [`Hyperlink`], [`Ui::hyperlink`] or [`Ui::hyperlink_to`].
///
/// See also [`Ui::link`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// // These are equivalent:
/// if ui.link("Documentation").clicked() {
/// // …
/// }
///
/// if ui.add(egui::Link::new("Documentation")).clicked() {
/// // …
/// }
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Link {
text: WidgetText,
}
impl Link {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self { text: text.into() }
}
}
impl Widget for Link {
fn ui(self, ui: &mut Ui) -> Response {
let Link { text } = self;
let label = Label::new(text).sense(Sense::click());
let (pos, text_galley, response) = label.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
if response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
}
if ui.is_rect_visible(response.rect) {
let color = ui.visuals().hyperlink_color;
let visuals = ui.style().interact(&response);
let underline = if response.hovered() || response.has_focus() {
Stroke::new(visuals.fg_stroke.width, color)
} else {
Stroke::NONE
};
ui.painter().add(epaint::TextShape {
pos,
galley: text_galley.galley,
override_text_color: Some(color),
underline,
angle: 0.0,
});
}
response
}
}
/// A clickable hyperlink, e.g. to `"https://github.com/emilk/egui"`.
///
/// See also [`Ui::hyperlink`] and [`Ui::hyperlink_to`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// // These are equivalent:
/// ui.hyperlink("https://github.com/emilk/egui");
/// ui.add(egui::Hyperlink::new("https://github.com/emilk/egui"));
///
/// // These are equivalent:
/// ui.hyperlink_to("My favorite repo", "https://github.com/emilk/egui");
/// ui.add(egui::Hyperlink::from_label_and_url("My favorite repo", "https://github.com/emilk/egui"));
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Hyperlink {
url: String,
text: WidgetText,
}
impl Hyperlink {
#[allow(clippy::needless_pass_by_value)]
pub fn new(url: impl ToString) -> Self {
let url = url.to_string();
Self {
url: url.clone(),
text: url.into(),
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn from_label_and_url(text: impl Into<WidgetText>, url: impl ToString) -> Self {
Self {
url: url.to_string(),
text: text.into(),
}
}
}
impl Widget for Hyperlink {
fn ui(self, ui: &mut Ui) -> Response {
let Self { url, text } = self;
let response = ui.add(Link::new(text));
if response.clicked() {
let modifiers = ui.ctx().input(|i| i.modifiers);
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: modifiers.any(),
});
});
}
if response.middle_clicked() {
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: true,
});
});
}
response.on_hover_text(url)
}
}

Some files were not shown because too many files have changed in this diff Show more