Compare commits

..

5 commits

Author SHA1 Message Date
Emil Ernerfeldt
ba9d1e446c Simplify interface 2022-08-20 10:04:12 +02:00
Emil Ernerfeldt
d3e26cd00a Merge branch 'master' into web_handle 2022-08-20 09:59:18 +02:00
Stanislav
6688079439
Update egui_demo_app/src/lib.rs
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
2022-08-04 18:39:47 +03:00
Stanislav
de3897d836 fix docs 2022-08-03 23:33:59 +03:00
Stanislav
fd9c8e5e08 Handle for app from WehHandle 2022-08-03 19:27:19 +03:00
366 changed files with 11525 additions and 19104 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

@ -8,7 +8,7 @@ assignees: ''
---
<!--
First look if there is already a similar bug report. If there is, upvote the issue with 👍
First look if there is already a similar bug report. If there is, add a comment to it instead!
Please also check if the bug is still present in latest master! Do so by adding the following lines to your Cargo.toml:

View file

@ -8,7 +8,7 @@ assignees: ''
---
<!--
First look if there is already a similar feature request. If there is, upvote the issue with 👍
First look if there is already a similar feature request. If there is, add a comment to it instead!
-->

View file

@ -3,209 +3,210 @@ on: [push, pull_request]
name: CI
env:
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
# as well as by the wasm32-backend of the wgpu crate.
# This is required to enable the web_sys clipboard API which eframe 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
RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings
RUSTDOCFLAGS: -D warnings
jobs:
fmt-crank-check-test:
name: Format + check + test
runs-on: ubuntu-22.04
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
check_default:
name: cargo check (default features)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
toolchain: 1.61.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev
- uses: actions-rs/cargo@v1
with:
command: check
args: --locked
check_all_features:
name: cargo check --all-features
strategy:
matrix:
os: [ubuntu-20.04, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev
- 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: 1.61.0
target: wasm32-unknown-unknown
override: true
- 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:
crate: cargo-cranky
- name: Check wasm32 egui_demo_app
uses: actions-rs/cargo@v1
- uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: Check wasm32 egui_demo_app --all-features
uses: actions-rs/cargo@v1
check_egui_demo_app:
name: cargo check -p egui_demo_app
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- name: check
run: cargo check -p egui_demo_app
check_wasm_eframe_with_features:
name: cargo check wasm eframe
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- name: check
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
check_web_all_features:
name: cargo check web --all-features
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: Check wasm32 eframe
uses: actions-rs/cargo@v1
test:
name: cargo test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
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
profile: minimal
toolchain: 1.61.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libgtk-3-dev # libgtk-3-dev is used by rfd
- uses: actions-rs/cargo@v1
with:
version: "0.2.84"
command: test
args: --all-features
- run: ./sh/wasm_bindgen_check.sh --skip-setup
fmt:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Cranky wasm32
uses: actions-rs/cargo@v1
cranky:
name: cargo cranky
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- run: cargo install cargo-cranky
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev libgtk-3-dev # libgtk-3-dev is used by rfd
- uses: actions-rs/cargo@v1
with:
command: cranky
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
args: --workspace --all-targets --all-features -- -D warnings
# ---------------------------------------------------------------------------
doc:
name: cargo doc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
override: true
- name: Install packages (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install libspeechd-dev
- run: cargo doc --lib --no-deps --all-features
cargo-deny:
name: cargo deny
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
rust-version: "1.65.0"
# ---------------------------------------------------------------------------
wasm_bindgen:
name: wasm-bindgen
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true
- run: ./sh/setup_web.sh
- run: ./sh/wasm_bindgen_check.sh
android:
name: android
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
toolchain: 1.61.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:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
override: true
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
- name: Check
uses: actions-rs/cargo@v1
with:
command: check
args: --all-targets --all-features
working-directory: eframe

4
.gitignore vendored
View file

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

29
.vscode/settings.json vendored
View file

@ -1,32 +1,5 @@
{
"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"
],
"files.trimTrailingWhitespace": true
}

View file

@ -1,4 +1,4 @@
# Architecture
# Arcitecture
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.

View file

@ -1,116 +1,10 @@
# 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!
NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.md), [`egui_glium`](egui_glium/CHANGELOG.md), and [`egui_glow`](egui_glow/CHANGELOG.md) have their own changelogs!
## 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 ⭐
* 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)).
* `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 `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 🔧
* 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)).
@ -121,20 +15,17 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* 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)).
* Added `Contex::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 🔧
### 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)).
* 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.
### Fixed 🐛
* Fixed `Response::changed` for `ui.toggle_value` ([#1573](https://github.com/emilk/egui/pull/1573)).
@ -142,8 +33,6 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
* 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)).

2582
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,23 @@
[workspace]
resolver = "2"
members = [
"crates/ecolor",
"crates/egui_demo_app",
"crates/egui_demo_lib",
"crates/egui_extras",
"crates/egui_glow",
"crates/egui-wgpu",
"crates/egui-winit",
"crates/egui",
"crates/emath",
"crates/epaint",
"egui_demo_app",
"egui_demo_lib",
"egui_extras",
"egui_glium",
"egui_glow",
"egui-wgpu",
"egui-winit",
"egui",
"emath",
"epaint",
"examples/*",
]
[profile.dev]
split-debuginfo = "unpacked" # faster debug builds on mac
[profile.release]
# lto = true # VERY slightly smaller wasm
# opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3`
@ -23,13 +26,3 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
# 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 +1,111 @@
# https://github.com/ericseppanen/cargo-cranky
# cargo install cargo-cranky && cargo cranky
deny = ["unsafe_code"]
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",
"clippy::all",
"clippy::await_holding_lock",
"clippy::char_lit_as_u8",
"clippy::checked_conversions",
"clippy::dbg_macro",
"clippy::debug_assert_with_mut_call",
"clippy::disallowed_methods",
"clippy::disallowed_script_idents",
"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::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_ok_or",
"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::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_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",
# "clippy::cloned_instead_of_copied",
# "clippy::mod_module_files",
# "trivial_casts",
# "unused_qualifications",
]
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

@ -11,7 +11,7 @@
👉 [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](#integrations) (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.
@ -60,17 +60,17 @@ If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/di
## 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).
[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/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:
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/master/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`
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-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`
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
@ -84,7 +84,7 @@ On Fedora Rawhide you need to run:
* A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)).
* No callbacks
* 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/demo/toggle_switch.rs)
* 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
* 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)
@ -114,7 +114,7 @@ The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-
* egui is pure Rust
* 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)
* [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/demo/toggle_switch.rs)
egui also tries to improve your experience in other small ways:
@ -158,42 +158,37 @@ An integration needs to do the following each frame:
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
* 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))
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs))
### Official integrations
These are the official egui integrations:
* [`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`.
* [`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).
* [`eframe`](https://github.com/emilk/egui/tree/master/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui_glow` and `egui-winit`.
* [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
* [`egui_glow`](https://github.com/emilk/egui/tree/master/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/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API).
* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
### 3rd party integrations
* [`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/).
* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw).
* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin).
* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2).
* [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2).
* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano).
* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad).
* [`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.
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) 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!
@ -224,7 +219,7 @@ loop {
}
```
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_glow` painter](https://github.com/emilk/egui/blob/master/egui_glow/src/painter.rs).
### Debugging your integration
@ -324,11 +319,11 @@ If you call `.await` in your GUI code, the UI will freeze, which is very bad UX.
* [`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.
There is experimental support for a 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.
Read more at <https://github.com/emilk/egui/issues/167>.
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/crates/eframe)?
### What is the difference between [egui](https://docs.rs/egui) and [eframe](https://github.com/emilk/egui/tree/master/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.
@ -348,14 +343,14 @@ 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.
`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/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>.
* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium): <https://github.com/emilk/egui/blob/master/egui_glium/examples/native_texture.rs>.
## Other
@ -372,8 +367,6 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
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>.
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
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy.
@ -399,7 +392,6 @@ Notable contributions by:
* [@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).
@ -412,12 +404,3 @@ Default fonts:
* `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)
* `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 +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],
])
}
}

File diff suppressed because it is too large Load diff

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,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,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,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,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);
}
}
}

File diff suppressed because it is too large Load diff

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,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,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,37 +0,0 @@
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Highlighting {}
impl super::Demo for Highlighting {
fn name(&self) -> &'static str {
"Highlighting"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.default_width(320.0)
.open(open)
.show(ctx, |ui| {
use super::View as _;
self.ui(ui);
});
}
}
impl super::View for Highlighting {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.label("This demo demonstrates highlighting a widget.");
ui.add_space(4.0);
let label_response = ui.label("Hover me to highlight the button!");
ui.add_space(4.0);
let mut button_response = ui.button("Hover the button to highlight the label!");
if label_response.hovered() {
button_response = button_response.highlight();
}
if button_response.hovered() {
label_response.highlight();
}
}
}

View file

@ -1,44 +0,0 @@
# Changelog for egui_extras
All notable changes to the `egui_extras` integration will be noted in this file.
## Unreleased
## 0.21.0 - 2023-02-08
* Update to egui 0.21
## 0.20.0 - 2022-12-08
* Added `RetainedImage::from_svg_bytes_with_size` to be able to specify a size for SVGs to be rasterized at.
* Lots of `Table` improvements ([#2369](https://github.com/emilk/egui/pull/2369)):
* Double-click column separators to auto-size the column.
* All `Table` now store state. You may see warnings about reused table ids. Use `ui.push_id` to fix this.
* `TableBuilder::column` takes a `Column` instead of a `Size`.
* `Column` controls default size, size range, resizing, and clipping of columns.
* `Column::auto` will pick a size automatically
* Added `Table::scroll_to_row`.
* Added `Table::min_scrolled_height` and `Table::max_scroll_height`.
* Added `TableBody::max_size`.
* `Table::scroll` renamed to `Table::vscroll`.
* `egui_extras::Strip` now has `clip: false` by default.
* Fix bugs when putting `Table` inside of a horizontal `ScrollArea`.
* Many other bug fixes.
* Add `Table::auto_shrink` - set to `false` to expand table to fit its containing `Ui` ([#2371](https://github.com/emilk/egui/pull/2371)).
* Added `TableBuilder::vertical_scroll_offset`: method to set vertical scroll offset position for a table ([#1946](https://github.com/emilk/egui/pull/1946)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* You can now specify a texture filter for `RetainedImage` ([#1636](https://github.com/emilk/egui/pull/1636)).
* Fixed uneven `Table` striping ([#1680](https://github.com/emilk/egui/pull/1680)).
## 0.18.0 - 2022-04-30
* Added `Strip`, `Table` and `DatePicker` ([#963](https://github.com/emilk/egui/pull/963)).
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
* Renamed feature "persistence" to "serde" ([#1540](https://github.com/emilk/egui/pull/1540)).
## 0.17.0 - 2022-02-22
* `RetainedImage`: convenience for loading svg, png, jpeg etc and showing them in egui.

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
#version 120
uniform sampler2D u_sampler;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
// 0-255 sRGB from 0-1 linear
vec3 srgb_from_linear(vec3 rgb) {
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
vec3 lower = rgb * vec3(3294.6);
vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025);
return mix(higher, lower, vec3(cutoff));
}
// 0-255 sRGBA from 0-1 linear
vec4 srgba_from_linear(vec4 rgba) {
return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a);
}
// 0-1 gamma from 0-1 linear
vec4 gamma_from_linear_rgba(vec4 linear_rgba) {
return vec4(srgb_from_linear(linear_rgba.rgb) / 255.0, linear_rgba.a);
}
void main() {
// The texture is set up with `SRGB8_ALPHA8`
vec4 texture_in_gamma = gamma_from_linear_rgba(texture2D(u_sampler, v_tc));
// Multiply vertex color with texture color (in gamma space).
gl_FragColor = v_rgba_gamma * texture_in_gamma;
}

View file

@ -1,32 +0,0 @@
#version 140
uniform sampler2D u_sampler;
in vec4 v_rgba_gamma;
in vec2 v_tc;
out vec4 f_color;
// 0-255 sRGB from 0-1 linear
vec3 srgb_from_linear(vec3 rgb) {
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
vec3 lower = rgb * vec3(3294.6);
vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025);
return mix(higher, lower, vec3(cutoff));
}
// 0-255 sRGBA from 0-1 linear
vec4 srgba_from_linear(vec4 rgba) {
return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a);
}
// 0-1 gamma from 0-1 linear
vec4 gamma_from_linear_rgba(vec4 linear_rgba) {
return vec4(srgb_from_linear(linear_rgba.rgb) / 255.0, linear_rgba.a);
}
void main() {
// The texture is set up with `SRGB8_ALPHA8`
vec4 texture_in_gamma = gamma_from_linear_rgba(texture(u_sampler, v_tc));
// Multiply vertex color with texture color (in gamma space).
f_color = v_rgba_gamma * texture_in_gamma;
}

View file

@ -1,19 +0,0 @@
#version 100
precision mediump float;
uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec2 a_tc;
attribute vec4 a_srgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
void main() {
gl_Position = vec4(
2.0 * a_pos.x / u_screen_size.x - 1.0,
1.0 - 2.0 * a_pos.y / u_screen_size.y,
0.0,
1.0);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -1,18 +0,0 @@
#version 120
uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec4 a_srgba; // 0-255 sRGB
attribute vec2 a_tc;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
void main() {
gl_Position = vec4(
2.0 * a_pos.x / u_screen_size.x - 1.0,
1.0 - 2.0 * a_pos.y / u_screen_size.y,
0.0,
1.0);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -1,18 +0,0 @@
#version 140
uniform vec2 u_screen_size;
in vec2 a_pos;
in vec4 a_srgba; // 0-255 sRGB
in vec2 a_tc;
out vec4 v_rgba_gamma;
out vec2 v_tc;
void main() {
gl_Position = vec4(
2.0 * a_pos.x / u_screen_size.x - 1.0,
1.0 - 2.0 * a_pos.y / u_screen_size.y,
0.0,
1.0);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -1,19 +0,0 @@
#version 300 es
precision mediump float;
uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec2 a_tc;
attribute vec4 a_srgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
void main() {
gl_Position = vec4(
2.0 * a_pos.x / u_screen_size.x - 1.0,
1.0 - 2.0 * a_pos.y / u_screen_size.y,
0.0,
1.0);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -1,255 +0,0 @@
//! Example how to use pure `egui_glow`.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(unsafe_code)]
use egui_winit::winit;
/// The majority of `GlutinWindowContext` is taken from `eframe`
struct GlutinWindowContext {
window: winit::window::Window,
gl_context: glutin::context::PossiblyCurrentContext,
gl_display: glutin::display::Display,
gl_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
}
impl GlutinWindowContext {
// refactor this function to use `glutin-winit` crate eventually.
// preferably add android support at the same time.
#[allow(unsafe_code)]
unsafe fn new(event_loop: &winit::event_loop::EventLoopWindowTarget<()>) -> Self {
use egui::NumExt;
use glutin::context::NotCurrentGlContextSurfaceAccessor;
use glutin::display::GetGlDisplay;
use glutin::display::GlDisplay;
use glutin::prelude::GlSurface;
use raw_window_handle::HasRawWindowHandle;
let winit_window_builder = winit::window::WindowBuilder::new()
.with_resizable(true)
.with_inner_size(winit::dpi::LogicalSize {
width: 800.0,
height: 600.0,
})
.with_title("egui_glow example") // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
.with_visible(false);
let config_template_builder = glutin::config::ConfigTemplateBuilder::new()
.prefer_hardware_accelerated(None)
.with_depth_size(0)
.with_stencil_size(0)
.with_transparency(false);
tracing::debug!("trying to get gl_config");
let (mut window, gl_config) =
glutin_winit::DisplayBuilder::new() // let glutin-winit helper crate handle the complex parts of opengl context creation
.with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
.with_window_builder(Some(winit_window_builder.clone()))
.build(
event_loop,
config_template_builder,
|mut config_iterator| {
config_iterator.next().expect(
"failed to find a matching configuration for creating glutin config",
)
},
)
.expect("failed to create gl_config");
let gl_display = gl_config.display();
tracing::debug!("found gl_config: {:?}", &gl_config);
let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle());
tracing::debug!("raw window handle: {:?}", raw_window_handle);
let context_attributes =
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle);
// by default, glutin will try to create a core opengl context. but, if it is not available, try to create a gl-es context using this fallback attributes
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::Gles(None))
.build(raw_window_handle);
let not_current_gl_context = unsafe {
gl_display
.create_context(&gl_config, &context_attributes)
.unwrap_or_else(|_| {
tracing::debug!("failed to create gl_context with attributes: {:?}. retrying with fallback context attributes: {:?}",
&context_attributes,
&fallback_context_attributes);
gl_config
.display()
.create_context(&gl_config, &fallback_context_attributes)
.expect("failed to create context even with fallback attributes")
})
};
// this is where the window is created, if it has not been created while searching for suitable gl_config
let window = window.take().unwrap_or_else(|| {
tracing::debug!("window doesn't exist yet. creating one now with finalize_window");
glutin_winit::finalize_window(event_loop, winit_window_builder.clone(), &gl_config)
.expect("failed to finalize glutin window")
});
let (width, height): (u32, u32) = window.inner_size().into();
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
let surface_attributes =
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(window.raw_window_handle(), width, height);
tracing::debug!(
"creating surface with attributes: {:?}",
&surface_attributes
);
let gl_surface = unsafe {
gl_display
.create_window_surface(&gl_config, &surface_attributes)
.unwrap()
};
tracing::debug!("surface created successfully: {gl_surface:?}.making context current");
let gl_context = not_current_gl_context.make_current(&gl_surface).unwrap();
gl_surface
.set_swap_interval(
&gl_context,
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()),
)
.unwrap();
GlutinWindowContext {
window,
gl_context,
gl_display,
gl_surface,
}
}
fn window(&self) -> &winit::window::Window {
&self.window
}
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
use glutin::surface::GlSurface;
self.gl_surface.resize(
&self.gl_context,
physical_size.width.try_into().unwrap(),
physical_size.height.try_into().unwrap(),
);
}
fn swap_buffers(&self) -> glutin::error::Result<()> {
use glutin::surface::GlSurface;
self.gl_surface.swap_buffers(&self.gl_context)
}
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
use glutin::display::GlDisplay;
self.gl_display.get_proc_address(addr)
}
}
fn main() {
let mut clear_color = [0.1, 0.1, 0.1];
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build();
let (gl_window, gl) = create_display(&event_loop);
let gl = std::sync::Arc::new(gl);
let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None);
event_loop.run(move |event, _, control_flow| {
let mut redraw = || {
let mut quit = false;
let repaint_after = egui_glow.run(gl_window.window(), |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
ui.heading("Hello World!");
if ui.button("Quit").clicked() {
quit = true;
}
ui.color_edit_button_rgb(&mut clear_color);
});
});
*control_flow = if quit {
winit::event_loop::ControlFlow::Exit
} else if repaint_after.is_zero() {
gl_window.window().request_redraw();
winit::event_loop::ControlFlow::Poll
} else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after)
{
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant)
} else {
winit::event_loop::ControlFlow::Wait
};
{
unsafe {
use glow::HasContext as _;
gl.clear_color(clear_color[0], clear_color[1], clear_color[2], 1.0);
gl.clear(glow::COLOR_BUFFER_BIT);
}
// draw things behind egui here
egui_glow.paint(gl_window.window());
// draw things on top of egui here
gl_window.swap_buffers().unwrap();
gl_window.window().set_visible(true);
}
};
match event {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
winit::event::Event::WindowEvent { event, .. } => {
use winit::event::WindowEvent;
if matches!(event, WindowEvent::CloseRequested | WindowEvent::Destroyed) {
*control_flow = winit::event_loop::ControlFlow::Exit;
}
if let winit::event::WindowEvent::Resized(physical_size) = &event {
gl_window.resize(*physical_size);
} else if let winit::event::WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} = &event
{
gl_window.resize(**new_inner_size);
}
let event_response = egui_glow.on_event(&event);
if event_response.repaint {
gl_window.window().request_redraw();
}
}
winit::event::Event::LoopDestroyed => {
egui_glow.destroy();
}
winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => {
gl_window.window().request_redraw();
}
_ => (),
}
});
}
fn create_display(
event_loop: &winit::event_loop::EventLoopWindowTarget<()>,
) -> (GlutinWindowContext, glow::Context) {
let glutin_window_context = unsafe { GlutinWindowContext::new(event_loop) };
let gl = unsafe {
glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
.expect("failed to construct C string from string for gl proc address");
glutin_window_context.get_proc_address(&s)
})
};
(glutin_window_context, gl)
}

View file

@ -1,41 +0,0 @@
#ifdef GL_ES
precision mediump float;
#endif
uniform sampler2D u_sampler;
#if NEW_SHADER_INTERFACE
in vec4 v_rgba_in_gamma;
in vec2 v_tc;
out vec4 f_color;
// a dirty hack applied to support webGL2
#define gl_FragColor f_color
#define texture2D texture
#else
varying vec4 v_rgba_in_gamma;
varying vec2 v_tc;
#endif
// 0-1 sRGB gamma from 0-1 linear
vec3 srgb_gamma_from_linear(vec3 rgb) {
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
vec3 lower = rgb * vec3(12.92);
vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055);
return mix(higher, lower, vec3(cutoff));
}
// 0-1 sRGBA gamma from 0-1 linear
vec4 srgba_gamma_from_linear(vec4 rgba) {
return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a);
}
void main() {
#if SRGB_TEXTURES
vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc));
#else
vec4 texture_in_gamma = texture2D(u_sampler, v_tc);
#endif
// We multiply the colors in gamma space, because that's the only way to get text to look right.
gl_FragColor = v_rgba_in_gamma * texture_in_gamma;
}

View file

@ -1,30 +0,0 @@
#if NEW_SHADER_INTERFACE
#define I in
#define O out
#define V(x) x
#else
#define I attribute
#define O varying
#define V(x) vec3(x)
#endif
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_screen_size;
I vec2 a_pos;
I vec4 a_srgba; // 0-255 sRGB
I vec2 a_tc;
O vec4 v_rgba_in_gamma;
O vec2 v_tc;
void main() {
gl_Position = vec4(
2.0 * a_pos.x / u_screen_size.x - 1.0,
1.0 - 2.0 * a_pos.y / u_screen_size.y,
0.0,
1.0);
v_rgba_in_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -1 +0,0 @@
`egui_web` used to be a standalone crate, but has now been moved into [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).

View file

@ -1,11 +0,0 @@
# emath - egui math library
[![Latest version](https://img.shields.io/crates/v/emath.svg)](https://crates.io/crates/emath)
[![Documentation](https://docs.rs/emath/badge.svg)](https://docs.rs/emath)
[![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 bare-bones 2D math library with types and functions useful for GUI building.
Made for [`egui`](https://github.com/emilk/egui/).

View file

@ -1,11 +0,0 @@
# epaint - egui paint library
[![Latest version](https://img.shields.io/crates/v/epaint.svg)](https://crates.io/crates/epaint)
[![Documentation](https://docs.rs/epaint/badge.svg)](https://docs.rs/epaint)
[![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 bare-bones 2D graphics library for turning simple 2D shapes and text into textured triangles.
Made for [`egui`](https://github.com/emilk/egui/).

View file

@ -3,7 +3,6 @@
targets = [
{ triple = "aarch64-apple-darwin" },
{ triple = "aarch64-linux-android" },
{ triple = "wasm32-unknown-unknown" },
{ triple = "x86_64-apple-darwin" },
{ triple = "x86_64-pc-windows-msvc" },
{ triple = "x86_64-unknown-linux-gnu" },
@ -24,29 +23,22 @@ ignore = [
multiple-versions = "deny"
wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed
deny = [
{ name = "cmake" }, # Lord no
{ name = "openssl-sys" }, # prefer rustls
{ name = "openssl" }, # prefer rustls
{ name = "openssl-sys" }, # prefer rustls
]
skip = [
{ name = "ahash" }, # old version via dark-light
{ name = "arrayvec" }, # old version via tiny-skiaz
{ name = "hashbrown" }, # old version via dark-light
{ name = "nix" }, # old version via winit
{ name = "time" }, # old version pulled in by unmaintianed crate 'chrono'
{ name = "tiny-skia" }, # winit uses a different version from egui_extras (TODO(emilk): update egui_extras!)
{ name = "ttf-parser" }, # different versions pulled in by ab_glyph and usvg
{ name = "wayland-sys" }, # old version via winit
{ name = "windows_x86_64_msvc" }, # old version via glutin
{ name = "windows-sys" }, # old version via glutin
{ name = "windows" }, # old version via accesskit
{ name = "ahash" }, # old version via dark-light
{ name = "arrayvec" }, # old version via tiny-skiaz
{ name = "hashbrown" }, # old version via dark-light
{ name = "time" }, # old version pulled in by unmaintianed crate 'chrono'
{ name = "ttf-parser" }, # different versions pulled in by ab_glyph and usvg
]
skip-tree = [
{ name = "criterion" }, # dev-dependency
{ name = "darling" }, # old version via tts
{ name = "foreign-types" }, # old version from wgpu
{ name = "rfd" }, # example dependency
{ name = "criterion" }, # dev-dependnecy
{ name = "glium" }, # legacy crate, lots of old dependencies
{ name = "rfd" }, # example dependency
{ name = "three-d" }, # example dependency
]
@ -56,19 +48,18 @@ allow-osi-fsf-free = "neither"
confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text
copyleft = "deny"
allow = [
"Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
"ISC", # https://tldrlegal.com/license/-isc-license
"LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321
"MIT", # https://tldrlegal.com/license/mit-license
"MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11
"OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html
"OpenSSL", # https://www.openssl.org/source/license.html
"Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
"Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
# "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html
"Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
"BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd)
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
"ISC", # https://tldrlegal.com/license/-isc-license
"MIT", # https://tldrlegal.com/license/mit-license
"MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11
"OpenSSL", # https://www.openssl.org/source/license.html
"Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
"Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
]
[[licenses.clarify]]

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -1,75 +1,14 @@
# 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!
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), and [`egui_glow`](../egui_glow/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.
* 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)).
@ -82,16 +21,15 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
* 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)).
* `dark-light` (dark mode detection) is now enabled by default on Mac and Windows ([#1726](https://github.com/emilk/egui/pull/1726)).
* 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)).
* 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)).
## 0.18.0 - 2022-04-30
@ -154,7 +92,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
## 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_window_title` to set window title dynamically
* `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.

View file

@ -1,30 +1,26 @@
[package]
name = "eframe"
version = "0.21.3"
version = "0.18.0"
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"
rust-version = "1.61"
homepage = "https://github.com/emilk/egui/tree/master/eframe"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
repository = "https://github.com/emilk/egui/tree/master/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"]
default = ["dark-light", "default_fonts", "glow"]
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
##
@ -35,8 +31,8 @@ dark-light = ["dep:dark-light"]
## 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"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow).
glow = ["dep:glow", "egui_glow"]
## Enable saving app state to disk.
persistence = [
@ -51,76 +47,59 @@ persistence = [
##
## 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"]
puffin = ["dep:puffin", "egui_glow?/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"]
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
screen_reader = [
"egui-winit/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)).
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui_wgpu`](https://github.com/emilk/egui/tree/master/egui_wgpu)).
## This overrides the `glow` feature.
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
wgpu = ["dep:wgpu", "egui-wgpu"]
[dependencies]
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
egui = { version = "0.18.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 }
egui_glow = { version = "0.18.0", path = "../egui_glow", optional = true, default-features = false }
egui-wgpu = { version = "0.18.0", path = "../egui-wgpu", optional = true, features = ["winit"] }
glow = { version = "0.11", optional = true }
ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] }
wgpu = { version = "0.13", optional = true }
# -------------------------------------------
# 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"
dark-light = { version = "0.2.1", optional = true }
egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] }
glutin = { version = "0.29.0" }
winit = "0.27.2"
# optional native:
dark-light = { version = "1.0", optional = true }
puffin = { version = "0.13", 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"
getrandom = { version = "0.2", features = ["js"] } # used by ahash
js-sys = "0.3"
percent-encoding = "2.1"
wasm-bindgen = "=0.2.84"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.58", features = [
web-sys = { version = "0.3.52", features = [
"BinaryType",
"Blob",
"Clipboard",
@ -163,7 +142,6 @@ web-sys = { version = "0.3.58", features = [
"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"] }
# optional
# feature screen_reader
tts = { version = "0.20", optional = true } # Can't use 0.22 due to compilation problems on linux: https://github.com/emilk/egui/runs/7170127089?check_suite_focus=true#step:5:713

View file

@ -17,23 +17,23 @@ 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).
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/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
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-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`.
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/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>.
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/egui_glow/examples/pure_glow.rs>.
## Problems with running egui on the web
@ -45,6 +45,7 @@ You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/win
* 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.
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
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).

View file

@ -9,22 +9,6 @@
#[cfg(target_arch = "wasm32")]
use std::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use crate::native::run::UserEvent;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use winit::event_loop::EventLoopBuilder;
/// Hook into the building of an event loop before it is run
///
/// You can configure any platform specific details required on top of the default configuration
/// done by `EFrame`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
/// This is how your app is created.
///
/// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc.
@ -62,7 +46,7 @@ pub struct CreationContext<'s> {
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/eframe).
pub trait App {
/// Called each time the UI needs repainting, which may be many times per second.
///
@ -73,25 +57,14 @@ pub trait App {
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame);
/// Get a handle to the app.
/// Handle to the app.
///
/// Can be used from web to interact or other external context.
/// Can be used from web to interact or other external context
/// Implementation is needed, because downcasting Box<dyn App> -> Box<dyn Any> to get &ConcreteApp is not simple in current rust.
///
/// You need to implement this if you want to be able to access the application from JS using [`crate::web::backend::AppRunner`].
///
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
///
/// Just copy-paste this as your implementation:
/// ```ignore
/// #[cfg(target_arch = "wasm32")]
/// fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
/// Some(&mut *self)
/// }
/// ```
/// Just return &mut *self
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
None
}
fn as_any_mut(&mut self) -> &mut dyn Any;
/// Called on shutdown, and perhaps at regular intervals. Allows you to save state.
///
@ -106,25 +79,21 @@ pub trait App {
/// where `APPNAME` is what is given to `eframe::run_native`.
fn save(&mut self, _storage: &mut dyn Storage) {}
/// Called when the user attempts to close the desktop window and/or quit the application.
///
/// By returning `false` the closing will be aborted. To continue the closing return `true`.
/// Called before an exit that can be aborted.
/// By returning `false` the exit will be aborted. To continue the exit return `true`.
///
/// A scenario where this method will be run is after pressing the close button on a native
/// window, which allows you to ask the user whether they want to do something before exiting.
/// See the example at <https://github.com/emilk/egui/blob/master/examples/confirm_exit/> for practical usage.
///
/// It will _not_ be called on the web or when the window is forcefully closed.
#[cfg(not(target_arch = "wasm32"))]
#[doc(alias = "exit")]
#[doc(alias = "quit")]
fn on_close_event(&mut self) -> bool {
fn on_exit_event(&mut self) -> bool {
true
}
/// Called once on shutdown, after [`Self::save`].
///
/// If you need to abort an exit use [`Self::on_close_event`].
/// If you need to abort an exit use [`Self::on_exit_event`].
///
/// To get a [`glow`] context you need to compile with the `glow` feature flag,
/// and run eframe with the glow backend.
@ -133,7 +102,7 @@ pub trait App {
/// Called once on shutdown, after [`Self::save`].
///
/// If you need to abort an exit use [`Self::on_close_event`].
/// If you need to abort an exit use [`Self::on_exit_event`].
#[cfg(not(feature = "glow"))]
fn on_exit(&mut self) {}
@ -148,25 +117,20 @@ pub trait App {
/// The size limit of the web app canvas.
///
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
///
/// A large canvas can lead to bad frame rates on some older browsers on some platforms
/// (see <https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0>).
fn max_size_points(&self) -> egui::Vec2 {
egui::Vec2::INFINITY
}
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
///
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
/// This is the background of your windows if you don't set a central panel.
///
/// ATTENTION:
/// Since these float values go to the render as-is, any color space conversion as done
/// e.g. by converting from [`egui::Color32`] to [`egui::Rgba`] may cause incorrect results.
/// egui recommends that rendering backends use a normal "gamma-space" (non-sRGB-aware) blending,
/// which means the values you return here should also be in `sRGB` gamma-space in the 0-1 range.
/// You can use [`egui::Color32::to_normalized_gamma_f32`] for this.
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
fn clear_color(&self, _visuals: &egui::Visuals) -> egui::Rgba {
// NOTE: a bright gray makes the shadows of the windows look weird.
// We use a bit of transparency so that if the user switches on the
// `transparent()` option they get immediate results.
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).to_normalized_gamma_f32()
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
// _visuals.window_fill() would also be a natural choice
}
@ -184,7 +148,7 @@ pub trait App {
}
/// If `true` a warm-up call to [`Self::update`] will be issued where
/// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`.
/// `ctx.memory().everything_is_visible()` will be set to `true`.
///
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
///
@ -221,8 +185,9 @@ pub enum HardwareAcceleration {
///
/// Only a single native window is currently supported.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone)]
pub struct NativeOptions {
/// Sets whether or not the window will always be on top of other windows at initialization.
/// Sets whether or not the window will always be on top of other windows.
pub always_on_top: bool,
/// Show window in maximized mode
@ -237,14 +202,6 @@ pub struct NativeOptions {
/// Default: `false`.
pub fullscreen: bool,
/// On Mac: the window doesn't have a titlebar, but floating window buttons.
///
/// See [winit's documentation][with_fullsize_content_view] for information on Mac-specific options.
///
/// [with_fullsize_content_view]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/macos/trait.WindowBuilderExtMacOS.html#tymethod.with_fullsize_content_view
#[cfg(target_os = "macos")]
pub fullsize_content: bool,
/// On Windows: enable drag and drop support. Drag and drop can
/// not be disabled on other platforms.
///
@ -266,10 +223,10 @@ pub struct NativeOptions {
/// The initial inner size of the native window in points (logical pixels).
pub initial_window_size: Option<egui::Vec2>,
/// The minimum inner window size in points (logical pixels).
/// The minimum inner window size
pub min_window_size: Option<egui::Vec2>,
/// The maximum inner window size in points (logical pixels).
/// The maximum inner window size
pub max_window_size: Option<egui::Vec2>,
/// Should the app window be resizable?
@ -280,10 +237,6 @@ pub struct NativeOptions {
/// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent.
pub transparent: bool,
/// On desktop: mouse clicks pass through the window, used for non-interactable overlays
/// Generally you would use this in conjunction with always_on_top
pub mouse_passthrough: bool,
/// Turn on vertical syncing, limiting the FPS to the display refresh rate.
///
/// The default is `true`.
@ -303,10 +256,6 @@ pub struct NativeOptions {
/// Sets the number of bits in the depth buffer.
///
/// `egui` doesn't need the depth buffer, so the default value is 0.
///
/// On `wgpu` backends, due to limited depth texture format options, this
/// will be interpreted as a boolean (non-zero = true) for whether or not
/// specifically a `Depth32Float` buffer is used.
pub depth_buffer: u8,
/// Sets the number of bits in the stencil buffer.
@ -314,16 +263,15 @@ pub struct NativeOptions {
/// `egui` doesn't need the stencil buffer, so the default value is 0.
pub stencil_buffer: u8,
/// Specify whether or not hardware acceleration is preferred, required, or not.
/// Specify wether or not hardware acceleration is preferred, required, or not.
///
/// Default: [`HardwareAcceleration::Preferred`].
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub renderer: Renderer,
/// Only used if the `dark-light` feature is enabled:
/// If the `dark-light` feature is enabled:
///
/// Try to detect and follow the system preferred setting for dark vs light mode.
///
@ -336,7 +284,7 @@ pub struct NativeOptions {
/// Which theme to use in case [`Self::follow_system_theme`] is `false`
/// or the `dark-light` feature is disabled.
///
/// Default: [`Theme::Dark`].
/// Default: `Theme::Dark`.
pub default_theme: Theme,
/// This controls what happens when you close the main eframe window.
@ -352,50 +300,6 @@ pub struct NativeOptions {
/// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used.
/// When `false`, [`winit::event_loop::EventLoop::run`] is used.
pub run_and_return: bool,
/// Hook into the building of an event loop before it is run.
///
/// Specify a callback here in case you need to make platform specific changes to the
/// event loop before it is run.
///
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub event_loop_builder: Option<EventLoopBuilderHook>,
#[cfg(feature = "glow")]
/// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture.
/// See <https://github.com/emilk/egui/pull/1993>.
///
/// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader").
pub shader_version: Option<egui_glow::ShaderVersion>,
/// On desktop: make the window position to be centered at initialization.
///
/// Platform specific:
///
/// Wayland desktop currently not supported.
pub centered: bool,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
}
#[cfg(not(target_arch = "wasm32"))]
impl Clone for NativeOptions {
fn clone(&self) -> Self {
Self {
icon_data: self.icon_data.clone(),
#[cfg(any(feature = "glow", feature = "wgpu"))]
event_loop_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "wgpu")]
wgpu_options: self.wgpu_options.clone(),
..*self
}
}
}
#[cfg(not(target_arch = "wasm32"))]
@ -406,10 +310,6 @@ impl Default for NativeOptions {
maximized: false,
decorated: true,
fullscreen: false,
#[cfg(target_os = "macos")]
fullsize_content: false,
drag_and_drop_support: true,
icon_data: None,
initial_window_pos: None,
@ -418,30 +318,15 @@ impl Default for NativeOptions {
max_window_size: None,
resizable: true,
transparent: false,
mouse_passthrough: false,
vsync: true,
multisampling: 0,
depth_buffer: 0,
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu"))]
renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark,
run_and_return: true,
#[cfg(any(feature = "glow", feature = "wgpu"))]
event_loop_builder: None,
#[cfg(feature = "glow")]
shader_version: None,
centered: false,
#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
}
}
}
@ -456,7 +341,6 @@ impl NativeOptions {
match dark_light::detect() {
dark_light::Mode::Dark => Some(Theme::Dark),
dark_light::Mode::Light => Some(Theme::Light),
dark_light::Mode::Default => None,
}
} else {
None
@ -491,12 +375,7 @@ pub struct WebOptions {
/// Which version of WebGl context to select
///
/// Default: [`WebGlContextOption::BestFirst`].
#[cfg(feature = "glow")]
pub webgl_context_option: WebGlContextOption,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
}
#[cfg(target_arch = "wasm32")]
@ -505,26 +384,7 @@ impl Default for WebOptions {
Self {
follow_system_theme: true,
default_theme: Theme::Dark,
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration {
// WebGPU is not stable enough yet, use WebGL emulation
backends: wgpu::Backends::GL,
device_descriptor: wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
features: wgpu::Features::default(),
limits: wgpu::Limits {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..wgpu::Limits::downlevel_webgl2_defaults()
},
},
..Default::default()
},
}
}
}
@ -578,7 +438,6 @@ pub enum WebGlContextOption {
/// What rendering backend to use.
///
/// You need to enable the "glow" and "wgpu" features to have a choice.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@ -592,7 +451,6 @@ pub enum Renderer {
Wgpu,
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl Default for Renderer {
fn default() -> Self {
#[cfg(feature = "glow")]
@ -608,7 +466,6 @@ impl Default for Renderer {
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -621,7 +478,6 @@ impl std::fmt::Display for Renderer {
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
impl std::str::FromStr for Renderer {
type Err = String;
@ -727,44 +583,17 @@ impl Frame {
self.wgpu_render_state.as_ref()
}
/// Tell `eframe` to close the desktop window.
///
/// The window will not close immediately, but at the end of the this frame.
///
/// Calling this will likely result in the app quitting, unless
/// you have more code after the call to [`crate::run_native`].
/// Signal the app to stop/exit/quit the app (only works for native apps, not web apps).
/// The framework will not quit immediately, but at the end of the this frame.
#[cfg(not(target_arch = "wasm32"))]
#[doc(alias = "exit")]
#[doc(alias = "quit")]
pub fn close(&mut self) {
tracing::debug!("eframe::Frame::close called");
self.output.close = true;
}
/// Minimize or unminimize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_minimized(&mut self, minimized: bool) {
self.output.minimized = Some(minimized);
}
/// Maximize or unmaximize window. (native only)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_maximized(&mut self, maximized: bool) {
self.output.maximized = Some(maximized);
}
/// Tell `eframe` to close the desktop window.
#[cfg(not(target_arch = "wasm32"))]
#[deprecated = "Renamed `close`"]
pub fn quit(&mut self) {
self.close();
self.output.quit = true;
}
/// Set the desired inner size of the window (in egui points).
#[cfg(not(target_arch = "wasm32"))]
pub fn set_window_size(&mut self, size: egui::Vec2) {
self.output.window_size = Some(size);
self.info.window_info.size = size; // so that subsequent calls see the updated value
}
/// Set the desired title of the window.
@ -785,14 +614,12 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))]
pub fn set_fullscreen(&mut self, fullscreen: bool) {
self.output.fullscreen = Some(fullscreen);
self.info.window_info.fullscreen = fullscreen; // so that subsequent calls see the updated value
}
/// set the position of the outer window.
#[cfg(not(target_arch = "wasm32"))]
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
self.output.window_pos = Some(pos);
self.info.window_info.position = Some(pos); // so that subsequent calls see the updated value
}
/// When called, the native window will follow the
@ -810,31 +637,7 @@ impl Frame {
self.output.visible = Some(visible);
}
/// On desktop: Set the window always on top.
///
/// (Wayland desktop currently not supported)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_always_on_top(&mut self, always_on_top: bool) {
self.output.always_on_top = Some(always_on_top);
}
/// On desktop: Set the window to be centered.
///
/// (Wayland desktop currently not supported)
#[cfg(not(target_arch = "wasm32"))]
pub fn set_centered(&mut self) {
if let Some(monitor_size) = self.info.window_info.monitor_size {
let inner_size = self.info.window_info.size;
if monitor_size.x > 1.0 && monitor_size.y > 1.0 {
let x = (monitor_size.x - inner_size.x) / 2.0;
let y = (monitor_size.y - inner_size.y) / 2.0;
self.set_window_pos(egui::Pos2 { x, y });
}
}
}
/// for integrations only: call once per frame
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
std::mem::take(&mut self.output)
}
@ -844,9 +647,6 @@ impl Frame {
#[derive(Clone, Debug)]
#[cfg(target_arch = "wasm32")]
pub struct WebInfo {
/// The browser user agent.
pub user_agent: String,
/// Information about the URL.
pub location: Location,
}
@ -865,17 +665,8 @@ pub struct WindowInfo {
/// Are we in fullscreen mode?
pub fullscreen: bool,
/// Are we minimized?
pub minimized: bool,
/// Are we maximized?
pub maximized: bool,
/// Window inner size in egui points (logical pixels).
pub size: egui::Vec2,
/// Current monitor size in egui points (logical pixels)
pub monitor_size: Option<egui::Vec2>,
}
/// Information about the URL.
@ -1018,9 +809,10 @@ pub(crate) mod backend {
#[derive(Clone, Debug, Default)]
#[must_use]
pub struct AppOutput {
/// Set to `true` to close the native window (which often quits the app).
/// Set to `true` to stop the app.
/// This does nothing for web apps.
#[cfg(not(target_arch = "wasm32"))]
pub close: bool,
pub quit: bool,
/// Set to some size to resize the outer window (e.g. glium window) to this size.
#[cfg(not(target_arch = "wasm32"))]
@ -1049,17 +841,5 @@ pub(crate) mod backend {
/// Set to some bool to change window visibility.
#[cfg(not(target_arch = "wasm32"))]
pub visible: Option<bool>,
/// Set to some bool to tell the window always on top.
#[cfg(not(target_arch = "wasm32"))]
pub always_on_top: Option<bool>,
/// Set to some bool to minimize or unminimize window.
#[cfg(not(target_arch = "wasm32"))]
pub minimized: Option<bool>,
/// Set to some bool to maximize or unmaximize window.
#[cfg(not(target_arch = "wasm32"))]
pub maximized: Option<bool>,
}
}

View file

@ -48,9 +48,8 @@
//! /// 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
//! pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
//! eframe::start_web(canvas_id, Box::new(|cc| Box::new(MyApp::new(cc))))
//! }
//! ```
//!
@ -103,32 +102,18 @@ pub use web_sys;
/// /// 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> {
/// pub 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
/// .map(|handle| WebHandle { handle })
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
/// }
/// ```
///
/// # Errors
/// Failing to initialize WebGL graphics.
#[cfg(target_arch = "wasm32")]
pub async fn start_web(
pub 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?;
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator)?;
Ok(handle)
}
@ -137,7 +122,6 @@ pub async fn start_web(
// 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.
@ -175,88 +159,46 @@ mod native;
/// }
/// }
/// ```
///
/// # 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<()> {
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
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)
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)
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>;
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
/// Profiling macro for feature "puffin"
#[cfg(not(target_arch = "wasm32"))]
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::*;
pub(crate) use profile_scope;

View file

@ -1,24 +1,6 @@
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,
}
use egui_winit::{native_pixels_per_point, WindowSettings};
use winit::event_loop::EventLoopWindowTarget;
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize {
@ -27,56 +9,36 @@ pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
}
}
pub fn read_window_info(
window: &winit::window::Window,
pixels_per_point: f32,
window_state: &WindowState,
) -> WindowInfo {
pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -> 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,
pub fn window_builder(
native_options: &epi::NativeOptions,
window_settings: Option<WindowSettings>,
window_settings: &Option<WindowSettings>,
) -> winit::window::WindowBuilder {
let epi::NativeOptions {
always_on_top,
maximized,
decorated,
fullscreen,
#[cfg(target_os = "macos")]
fullsize_content,
drag_and_drop_support,
icon_data,
initial_window_pos,
@ -85,31 +47,19 @@ pub fn window_builder<E>(
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_always_on_top(*always_on_top)
.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);
}
.with_window_icon(window_icon);
if let Some(min_size) = *min_window_size {
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
@ -120,75 +70,23 @@ pub fn window_builder<E>(
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);
if let Some(window_settings) = window_settings {
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 {
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
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()
}
@ -215,20 +113,16 @@ 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: _,
quit: _,
window_size,
window_title,
decorated,
fullscreen,
drag_window,
window_pos,
visible: _, // handled in post_present
always_on_top,
minimized,
maximized,
visible,
} = app_output;
if let Some(decorated) = decorated {
@ -246,7 +140,7 @@ pub fn handle_app_output(
}
if let Some(fullscreen) = fullscreen {
window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
window.set_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)));
}
if let Some(window_title) = window_title {
@ -264,23 +158,8 @@ pub fn handle_app_output(
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;
if let Some(visible) = visible {
window.set_visible(visible);
}
}
@ -304,10 +183,9 @@ pub struct EpiIntegration {
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,
/// When set, it is time to quit
quit: bool,
can_drag_window: bool,
window_state: WindowState,
}
impl EpiIntegration {
@ -322,27 +200,16 @@ impl EpiIntegration {
) -> 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(),
};
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
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()
native_pixels_per_point: Some(native_pixels_per_point(window)),
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
},
output: Default::default(),
storage,
#[cfg(feature = "glow")]
gl,
@ -352,7 +219,8 @@ impl EpiIntegration {
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);
let pixels_per_point = window.scale_factor() as f32;
egui_winit.set_pixels_per_point(pixels_per_point);
Self {
frame,
@ -360,81 +228,41 @@ impl EpiIntegration {
egui_ctx,
egui_winit,
pending_full_output: Default::default(),
close: false,
quit: 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 saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().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.memory() = 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
/// If `true`, it is time to shut down.
pub fn should_quit(&self) -> bool {
self.quit
}
pub fn on_event(
&mut self,
app: &mut dyn epi::App,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
pub fn on_event(&mut self, app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>) {
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::CloseRequested => self.quit = app.on_exit_event(),
WindowEvent::Destroyed => self.quit = 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);
self.egui_winit.on_event(&self.egui_ctx, event);
}
pub fn update(
@ -444,16 +272,12 @@ impl EpiIntegration {
) -> 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);
self.frame.info.window_info = read_window_info(window, self.egui_ctx.pixels_per_point());
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);
@ -461,20 +285,13 @@ impl EpiIntegration {
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);
if app_output.quit {
self.quit = app.on_exit_event();
}
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,
);
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
}
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
let frame_time = (std::time::Instant::now() - frame_start).as_secs_f64() as f32;
self.frame.info.cpu_usage = Some(frame_time);
full_output
@ -487,12 +304,6 @@ impl EpiIntegration {
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,
@ -528,8 +339,7 @@ impl EpiIntegration {
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
}
{
crate::profile_scope!("App::save");

View file

@ -26,7 +26,6 @@ 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,

735
eframe/src/native/run.rs Normal file
View file

@ -0,0 +1,735 @@
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
//! When making changes to one you often also want to apply it to the other.
use std::time::Duration;
use std::time::Instant;
use egui_winit::winit;
use winit::event_loop::{ControlFlow, EventLoop};
use super::epi_integration::{self, EpiIntegration};
use crate::epi;
#[derive(Debug)]
struct RequestRepaintEvent;
#[cfg(feature = "glow")]
#[allow(unsafe_code)]
fn create_display(
native_options: &NativeOptions,
window_builder: winit::window::WindowBuilder,
event_loop: &EventLoop<RequestRepaintEvent>,
) -> (
glutin::WindowedContext<glutin::PossiblyCurrent>,
glow::Context,
) {
crate::profile_function!();
use crate::HardwareAcceleration;
let hardware_acceleration = match native_options.hardware_acceleration {
HardwareAcceleration::Required => Some(true),
HardwareAcceleration::Preferred => None,
HardwareAcceleration::Off => Some(false),
};
let gl_window = unsafe {
glutin::ContextBuilder::new()
.with_hardware_acceleration(hardware_acceleration)
.with_depth_buffer(native_options.depth_buffer)
.with_multisampling(native_options.multisampling)
.with_srgb(true)
.with_stencil_buffer(native_options.stencil_buffer)
.with_vsync(native_options.vsync)
.build_windowed(window_builder, event_loop)
.unwrap()
.make_current()
.unwrap()
};
let gl = unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
(gl_window, gl)
}
// ----------------------------------------------------------------------------
pub use epi::NativeOptions;
enum EventResult {
Wait,
RepaintAsap,
RepaintAt(Instant),
Exit,
}
trait WinitApp {
fn is_focused(&self) -> bool;
fn integration(&self) -> &EpiIntegration;
fn window(&self) -> &winit::window::Window;
fn save_and_destroy(&mut self);
fn paint(&mut self) -> EventResult;
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult;
}
/// Access a thread-local event loop.
///
/// We reuse the event-loop so we can support closing and opening an eframe window
/// multiple times. This is just a limitation of winit.
fn with_event_loop(f: impl FnOnce(&mut EventLoop<RequestRepaintEvent>)) {
use std::cell::RefCell;
thread_local!(static EVENT_LOOP: RefCell<EventLoop<RequestRepaintEvent>> = RefCell::new(winit::event_loop::EventLoopBuilder::with_user_event().build()));
EVENT_LOOP.with(|event_loop| {
f(&mut *event_loop.borrow_mut());
});
}
fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app: impl WinitApp) {
use winit::platform::run_return::EventLoopExtRunReturn as _;
tracing::debug!("event_loop.run_return");
let mut next_repaint_time = Instant::now();
event_loop.run_return(|event, _, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::UserEvent(RequestRepaintEvent)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => EventResult::RepaintAsap,
winit::event::Event::WindowEvent { window_id, .. }
if window_id != winit_app.window().id() =>
{
// This can happen if we close a window, and then reopen a new one,
// or if we have multiple windows open.
EventResult::Wait
}
event => winit_app.on_event(event),
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintAsap => {
next_repaint_time = Instant::now();
}
EventResult::RepaintAt(repaint_time) => {
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
*control_flow = ControlFlow::Exit;
return;
}
}
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
None => {
winit_app.window().request_redraw();
ControlFlow::Poll
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
}
}
});
tracing::debug!("eframe window closed");
winit_app.save_and_destroy();
drop(winit_app);
// Needed to clean the event_loop:
event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit();
});
}
fn run_and_exit(
event_loop: EventLoop<RequestRepaintEvent>,
mut winit_app: impl WinitApp + 'static,
) -> ! {
tracing::debug!("event_loop.run");
let mut next_repaint_time = Instant::now();
event_loop.run(move |event, _, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
winit_app.paint()
}
winit::event::Event::UserEvent(RequestRepaintEvent)
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => EventResult::RepaintAsap,
event => winit_app.on_event(event),
};
match event_result {
EventResult::Wait => {}
EventResult::RepaintAsap => {
next_repaint_time = Instant::now();
}
EventResult::RepaintAt(repaint_time) => {
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
tracing::debug!("Quitting…");
winit_app.save_and_destroy();
#[allow(clippy::exit)]
std::process::exit(0);
}
}
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
None => {
winit_app.window().request_redraw();
ControlFlow::Poll
}
Some(time_until_next_repaint) => {
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
}
}
})
}
// ----------------------------------------------------------------------------
/// Run an egui app
#[cfg(feature = "glow")]
mod glow_integration {
use std::sync::Arc;
use super::*;
struct GlowWinitApp {
gl_window: glutin::WindowedContext<glutin::PossiblyCurrent>,
gl: Arc<glow::Context>,
painter: egui_glow::Painter,
integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>,
is_focused: bool,
}
impl GlowWinitApp {
fn new(
event_loop: &EventLoop<RequestRepaintEvent>,
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref());
let window_builder = epi_integration::window_builder(native_options, &window_settings)
.with_title(app_name);
let (gl_window, gl) = create_display(native_options, window_builder, event_loop);
let gl = Arc::new(gl);
let painter = egui_glow::Painter::new(gl.clone(), None, "")
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new(
event_loop,
painter.max_texture_side(),
gl_window.window(),
system_theme,
storage,
Some(gl.clone()),
#[cfg(feature = "wgpu")]
None,
);
let theme = system_theme.unwrap_or(native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
{
let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
integration.egui_ctx.set_request_repaint_callback(move || {
event_loop_proxy.lock().send_event(RequestRepaintEvent).ok();
});
}
let mut app = app_creator(&epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info(),
storage: integration.frame.storage(),
gl: Some(gl.clone()),
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
});
if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), gl_window.window());
}
Self {
gl_window,
gl,
painter,
integration,
app,
is_focused: true,
}
}
}
impl WinitApp for GlowWinitApp {
fn is_focused(&self) -> bool {
self.is_focused
}
fn integration(&self) -> &EpiIntegration {
&self.integration
}
fn window(&self) -> &winit::window::Window {
self.gl_window.window()
}
fn save_and_destroy(&mut self) {
self.integration
.save(&mut *self.app, self.gl_window.window());
self.app.on_exit(Some(&self.gl));
self.painter.destroy();
}
fn paint(&mut self) -> EventResult {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame");
let Self {
gl_window,
gl,
app,
integration,
painter,
..
} = self;
let window = gl_window.window();
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
egui_glow::painter::clear(
gl,
screen_size_in_pixels,
app.clear_color(&integration.egui_ctx.style().visuals),
);
let egui::FullOutput {
platform_output,
repaint_after,
textures_delta,
shapes,
} = integration.update(app.as_mut(), window);
integration.handle_platform_output(window, platform_output);
let clipped_primitives = {
crate::profile_scope!("tessellate");
integration.egui_ctx.tessellate(shapes)
};
painter.paint_and_update_textures(
screen_size_in_pixels,
integration.egui_ctx.pixels_per_point(),
&clipped_primitives,
&textures_delta,
);
integration.post_rendering(app.as_mut(), window);
{
crate::profile_scope!("swap_buffers");
gl_window.swap_buffers().unwrap();
}
let control_flow = if integration.should_quit() {
EventResult::Exit
} else if repaint_after.is_zero() {
EventResult::RepaintAsap
} else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after)
{
// if repaint_after is something huge and can't be added to Instant,
// we will use `ControlFlow::Wait` instead.
// technically, this might lead to some weird corner cases where the user *WANTS*
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess.
EventResult::RepaintAt(repaint_after_instant)
} else {
EventResult::Wait
};
integration.maybe_autosave(app.as_mut(), window);
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
// However, a user may want an egui with an animation in the background,
// so we still need to repaint quite fast.
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event {
winit::event::Event::WindowEvent { event, .. } => {
match &event {
winit::event::WindowEvent::Focused(new_focused) => {
self.is_focused = *new_focused;
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 {
self.gl_window.resize(*physical_size);
}
}
winit::event::WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} => {
self.gl_window.resize(**new_inner_size);
}
winit::event::WindowEvent::CloseRequested
if self.integration.should_quit() =>
{
return EventResult::Exit
}
_ => {}
}
self.integration.on_event(self.app.as_mut(), &event);
if self.integration.should_quit() {
EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
}
}
_ => EventResult::Wait,
}
}
}
pub fn run_glow(
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) {
if native_options.run_and_return {
with_event_loop(|event_loop| {
let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, glow_eframe);
});
} else {
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build();
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
}
}
#[cfg(feature = "glow")]
pub use glow_integration::run_glow;
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
mod wgpu_integration {
use super::*;
struct WgpuWinitApp {
window: winit::window::Window,
painter: egui_wgpu::winit::Painter<'static>,
integration: epi_integration::EpiIntegration,
app: Box<dyn epi::App>,
is_focused: bool,
}
impl WgpuWinitApp {
fn new(
event_loop: &EventLoop<RequestRepaintEvent>,
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref());
let window = epi_integration::window_builder(native_options, &window_settings)
.with_title(app_name)
.build(event_loop)
.unwrap();
// SAFETY: `window` must outlive `painter`.
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new(
wgpu::Backends::PRIMARY | wgpu::Backends::GL,
wgpu::PowerPreference::HighPerformance,
wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::default(),
limits: wgpu::Limits::default(),
},
wgpu::PresentMode::Fifo,
native_options.multisampling.max(1) as _,
);
#[cfg(not(target_os = "android"))]
painter.set_window(Some(&window));
painter
};
let wgpu_render_state = painter.render_state();
let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new(
event_loop,
painter.max_texture_side().unwrap_or(2048),
&window,
system_theme,
storage,
#[cfg(feature = "glow")]
None,
wgpu_render_state.clone(),
);
let theme = system_theme.unwrap_or(native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
{
let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
integration.egui_ctx.set_request_repaint_callback(move || {
event_loop_proxy.lock().send_event(RequestRepaintEvent).ok();
});
}
let mut app = app_creator(&epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info(),
storage: integration.frame.storage(),
#[cfg(feature = "glow")]
gl: None,
wgpu_render_state,
});
if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), &window);
}
Self {
window,
painter,
integration,
app,
is_focused: true,
}
}
}
impl WinitApp for WgpuWinitApp {
fn is_focused(&self) -> bool {
self.is_focused
}
fn integration(&self) -> &EpiIntegration {
&self.integration
}
fn window(&self) -> &winit::window::Window {
&self.window
}
fn save_and_destroy(&mut self) {
self.integration.save(&mut *self.app, &self.window);
#[cfg(feature = "glow")]
self.app.on_exit(None);
#[cfg(not(feature = "glow"))]
self.app.on_exit();
self.painter.destroy();
}
fn paint(&mut self) -> EventResult {
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame");
let Self {
window,
app,
integration,
painter,
..
} = self;
let egui::FullOutput {
platform_output,
repaint_after,
textures_delta,
shapes,
} = integration.update(app.as_mut(), window);
integration.handle_platform_output(window, platform_output);
let clipped_primitives = {
crate::profile_scope!("tessellate");
integration.egui_ctx.tessellate(shapes)
};
painter.paint_and_update_textures(
integration.egui_ctx.pixels_per_point(),
app.clear_color(&integration.egui_ctx.style().visuals),
&clipped_primitives,
&textures_delta,
);
integration.post_rendering(app.as_mut(), window);
let control_flow = if integration.should_quit() {
EventResult::Exit
} else if repaint_after.is_zero() {
EventResult::RepaintAsap
} else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after)
{
// if repaint_after is something huge and can't be added to Instant,
// we will use `ControlFlow::Wait` instead.
// technically, this might lead to some weird corner cases where the user *WANTS*
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess.
EventResult::RepaintAt(repaint_after_instant)
} else {
EventResult::Wait
};
integration.maybe_autosave(app.as_mut(), window);
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
// However, a user may want an egui with an animation in the background,
// so we still need to repaint quite fast.
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event {
#[cfg(target_os = "android")]
winit::event::Event::Resumed => unsafe {
self.painter.set_window(Some(&self.window));
EventResult::RepaintAsap
},
#[cfg(target_os = "android")]
winit::event::Event::Suspended => unsafe {
self.painter.set_window(None);
EventResult::Wait
},
winit::event::Event::WindowEvent { event, .. } => {
match &event {
winit::event::WindowEvent::Focused(new_focused) => {
self.is_focused = *new_focused;
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 {
self.painter
.on_window_resized(physical_size.width, physical_size.height);
}
}
winit::event::WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} => {
self.painter
.on_window_resized(new_inner_size.width, new_inner_size.height);
}
winit::event::WindowEvent::CloseRequested
if self.integration.should_quit() =>
{
return EventResult::Exit
}
_ => {}
};
self.integration.on_event(self.app.as_mut(), &event);
if self.integration.should_quit() {
EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
}
}
_ => EventResult::Wait,
}
}
}
pub fn run_wgpu(
app_name: &str,
native_options: &epi::NativeOptions,
app_creator: epi::AppCreator,
) {
if native_options.run_and_return {
with_event_loop(|event_loop| {
let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, wgpu_eframe);
});
} else {
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build();
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}
}
}
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
pub use wgpu_integration::run_wgpu;

View file

@ -1,11 +1,12 @@
use super::{glow_wrapping::WrappedGlowPainter, *};
use crate::epi;
use egui::{
mutex::{Mutex, MutexGuard},
TexturesDelta,
};
use crate::{epi, App};
use super::{web_painter::WebPainter, *};
pub use egui::{pos2, Color32};
// ----------------------------------------------------------------------------
@ -87,10 +88,6 @@ impl IsDestroyed {
// ----------------------------------------------------------------------------
fn user_agent() -> Option<String> {
web_sys::window()?.navigator().user_agent().ok()
}
fn web_location() -> epi::Location {
let location = web_sys::window().unwrap().location();
@ -105,7 +102,7 @@ fn web_location() -> epi::Location {
let query_map = parse_query_map(&query)
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
epi::Location {
@ -165,7 +162,7 @@ fn test_parse_query() {
pub struct AppRunner {
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
painter: ActiveWebPainter,
painter: WrappedGlowPainter,
pub(crate) input: WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
@ -185,14 +182,13 @@ impl Drop for AppRunner {
}
impl AppRunner {
/// # Errors
/// Failure to initialize WebGL renderer.
pub async fn new(
pub 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?;
) -> Result<Self, JsValue> {
let painter = WrappedGlowPainter::new(canvas_id, web_options.webgl_context_option)
.map_err(JsValue::from)?; // fail early
let system_theme = if web_options.follow_system_theme {
super::system_theme()
@ -202,7 +198,6 @@ impl AppRunner {
let info = epi::IntegrationInfo {
web_info: epi::WebInfo {
user_agent: user_agent().unwrap_or_default(),
location: web_location(),
},
system_theme,
@ -212,9 +207,6 @@ impl AppRunner {
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);
@ -224,13 +216,9 @@ impl AppRunner {
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"))]
gl: Some(painter.painter.gl().clone()),
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
});
@ -238,13 +226,9 @@ impl AppRunner {
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"))]
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
};
@ -282,14 +266,8 @@ impl AppRunner {
}
/// 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 app_mut<ConreteApp: 'static + crate::App>(&mut self) -> &mut ConreteApp {
self.app.as_any_mut().downcast_mut::<ConreteApp>().unwrap()
}
pub fn auto_save(&mut self) {
@ -313,11 +291,10 @@ impl AppRunner {
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));
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().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.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
Ok(())
@ -374,12 +351,16 @@ impl AppRunner {
Ok((repaint_after, clipped_primitives))
}
pub fn clear_color_buffer(&self) {
self.painter
.clear(self.app.clear_color(&self.egui_ctx.style().visuals));
}
/// 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,
@ -389,7 +370,7 @@ impl AppRunner {
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options(|o| o.screen_reader) {
if self.egui_ctx.options().screen_reader {
self.screen_reader
.speak(&platform_output.events_description());
}
@ -401,8 +382,6 @@ impl AppRunner {
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);
@ -450,6 +429,8 @@ pub enum EventToUnsubscribe {
impl EventToUnsubscribe {
pub fn unsubscribe(self) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
match self {
EventToUnsubscribe::TargetEvent(handle) => {
handle.target.remove_event_listener_with_callback(
@ -466,7 +447,6 @@ impl EventToUnsubscribe {
}
}
}
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
@ -479,12 +459,17 @@ pub struct AppRunnerContainer {
impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
///
#[must_use]
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> {
use wasm_bindgen::JsCast;
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
@ -507,7 +492,7 @@ impl AppRunnerContainer {
let handle = TargetEvent {
target: target.clone(),
event_name: event_name.to_owned(),
event_name: event_name.to_string(),
closure,
};
@ -521,17 +506,12 @@ impl AppRunnerContainer {
/// Install event listeners to register different input events
/// and start running the given app.
pub async fn start(
pub 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?;
let mut runner = AppRunner::new(canvas_id, web_options, app_creator)?;
runner.warm_up()?;
start_runner(runner)
}

View file

@ -1,8 +1,5 @@
use std::sync::atomic::{AtomicBool, Ordering};
use egui::Key;
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
struct IsDestroyed(pub bool);
@ -16,6 +13,7 @@ pub fn paint_and_schedule(
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
runner_lock.needs_repaint.clear();
runner_lock.clear_color_buffer();
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
runner_lock
@ -31,6 +29,7 @@ pub fn paint_and_schedule(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
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())?;
@ -66,13 +65,11 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.modifiers = modifiers;
let key = event.key();
let egui_key = translate_key(&key);
if let Some(key) = egui_key {
if let Some(key) = translate_key(&key) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
repeat: false, // egui will fill this in for us!
modifiers,
});
}
@ -88,18 +85,10 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
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) {
let prevent_default = if matches!(event.key().as_str(), "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(),
@ -123,7 +112,6 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
if prevent_default {
event.prevent_default();
// event.stop_propagation();
}
},
)?;
@ -138,7 +126,6 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: false,
repeat: false,
modifiers,
});
}
@ -210,21 +197,15 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
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",
];
let event_name = "contextmenu";
for event_name in prevent_default_events {
let closure =
move |event: web_sys::MouseEvent,
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
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)?;
@ -233,7 +214,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
runner_container.add_event_listener(
&canvas,
"mousedown",
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|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;
@ -328,7 +309,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
modifiers,
});
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
@ -350,7 +331,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
.events
.push(egui::Event::PointerMoved(pos));
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
@ -377,7 +358,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.repaint_asap();
event.stop_propagation();
event.prevent_default();
@ -408,7 +389,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
}
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.
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL

View file

@ -1,32 +1,27 @@
use crate::WebGlContextOption;
use egui::{ClippedPrimitive, Rgba};
use egui_glow::glow;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
#[cfg(not(target_arch = "wasm32"))]
use web_sys::{WebGl2RenderingContext, WebGlRenderingContext};
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,
pub(crate) struct WrappedGlowPainter {
pub(crate) canvas: HtmlCanvasElement,
pub(crate) canvas_id: String,
pub(crate) 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> {
impl WrappedGlowPainter {
pub fn new(canvas_id: &str, options: WebGlContextOption) -> 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, shader_prefix) = init_glow_context_from_canvas(&canvas, options)?;
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
let dimension = [canvas.width() as i32, canvas.height() as i32];
let painter = egui_glow::Painter::new(gl, Some(dimension), shader_prefix)
.map_err(|error| format!("Error starting glow painter: {}", error))?;
Ok(Self {
@ -37,41 +32,64 @@ impl WebPainterGlow {
}
}
impl WebPainter for WebPainterGlow {
fn max_texture_side(&self) -> usize {
impl WrappedGlowPainter {
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
self.painter.gl()
}
pub fn max_texture_side(&self) -> usize {
self.painter.max_texture_side()
}
fn canvas_id(&self) -> &str {
pub fn canvas_id(&self) -> &str {
&self.canvas_id
}
fn paint_and_update_textures(
pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) {
self.painter.set_texture(tex_id, delta);
}
pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.painter.free_texture(tex_id);
}
pub fn clear(&self, clear_color: Rgba) {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
}
pub fn paint_primitives(
&mut self,
clipped_primitives: &[ClippedPrimitive],
pixels_per_point: f32,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
Ok(())
}
pub 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);
self.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);
self.paint_primitives(clipped_primitives, pixels_per_point)?;
for &id in &textures_delta.free {
self.painter.free_texture(id);
self.free_texture(id);
}
Ok(())
}
fn destroy(&mut self) {
self.painter.destroy();
pub fn destroy(&mut self) {
self.painter.destroy()
}
}
@ -112,7 +130,7 @@ fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st
.dyn_into::<web_sys::WebGlRenderingContext>()
.unwrap();
let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) {
let shader_prefix = if super::webgl1_requires_brightening(&gl1_ctx) {
tracing::debug!("Enabling webkitGTK brightening workaround.");
"#define APPLY_BRIGHTENING_GAMMA"
} else {
@ -141,44 +159,8 @@ fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st
Some((gl, shader_prefix))
}
fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794
trait DummyWebGLConstructor {
fn from_webgl1_context(context: web_sys::WebGlRenderingContext) -> Self;
// 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
fn from_webgl2_context(context: web_sys::WebGl2RenderingContext) -> Self;
}

View file

@ -54,8 +54,8 @@ pub fn pos_from_touch_event(
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,
x: touch.page_x() as f32 - canvas_origin.x as f32,
y: touch.page_y() as f32 - canvas_origin.y as f32,
}
}
@ -113,88 +113,83 @@ pub fn should_ignore_key(key: &str) -> bool {
/// 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),
"ArrowDown" => Some(egui::Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp),
"Esc" | "Escape" => Some(Key::Escape),
"Tab" => Some(Key::Tab),
"Backspace" => Some(Key::Backspace),
"Enter" => Some(Key::Enter),
"Space" | " " => Some(Key::Space),
"Esc" | "Escape" => Some(egui::Key::Escape),
"Tab" => Some(egui::Key::Tab),
"Backspace" => Some(egui::Key::Backspace),
"Enter" => Some(egui::Key::Enter),
"Space" | " " => Some(egui::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),
"Help" | "Insert" => Some(egui::Key::Insert),
"Delete" => Some(egui::Key::Delete),
"Home" => Some(egui::Key::Home),
"End" => Some(egui::Key::End),
"PageUp" => Some(egui::Key::PageUp),
"PageDown" => Some(egui::Key::PageDown),
"-" => Some(Key::Minus),
"+" | "=" => Some(Key::PlusEquals),
"0" => Some(egui::Key::Num0),
"1" => Some(egui::Key::Num1),
"2" => Some(egui::Key::Num2),
"3" => Some(egui::Key::Num3),
"4" => Some(egui::Key::Num4),
"5" => Some(egui::Key::Num5),
"6" => Some(egui::Key::Num6),
"7" => Some(egui::Key::Num7),
"8" => Some(egui::Key::Num8),
"9" => Some(egui::Key::Num9),
"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(egui::Key::A),
"b" | "B" => Some(egui::Key::B),
"c" | "C" => Some(egui::Key::C),
"d" | "D" => Some(egui::Key::D),
"e" | "E" => Some(egui::Key::E),
"f" | "F" => Some(egui::Key::F),
"g" | "G" => Some(egui::Key::G),
"h" | "H" => Some(egui::Key::H),
"i" | "I" => Some(egui::Key::I),
"j" | "J" => Some(egui::Key::J),
"k" | "K" => Some(egui::Key::K),
"l" | "L" => Some(egui::Key::L),
"m" | "M" => Some(egui::Key::M),
"n" | "N" => Some(egui::Key::N),
"o" | "O" => Some(egui::Key::O),
"p" | "P" => Some(egui::Key::P),
"q" | "Q" => Some(egui::Key::Q),
"r" | "R" => Some(egui::Key::R),
"s" | "S" => Some(egui::Key::S),
"t" | "T" => Some(egui::Key::T),
"u" | "U" => Some(egui::Key::U),
"v" | "V" => Some(egui::Key::V),
"w" | "W" => Some(egui::Key::W),
"x" | "X" => Some(egui::Key::X),
"y" | "Y" => Some(egui::Key::Y),
"z" | "Z" => Some(egui::Key::Z),
"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),
"F1" => Some(egui::Key::F1),
"F2" => Some(egui::Key::F2),
"F3" => Some(egui::Key::F3),
"F4" => Some(egui::Key::F4),
"F5" => Some(egui::Key::F5),
"F6" => Some(egui::Key::F6),
"F7" => Some(egui::Key::F7),
"F8" => Some(egui::Key::F8),
"F9" => Some(egui::Key::F9),
"F10" => Some(egui::Key::F10),
"F11" => Some(egui::Key::F11),
"F12" => Some(egui::Key::F12),
"F13" => Some(egui::Key::F13),
"F14" => Some(egui::Key::F14),
"F15" => Some(egui::Key::F15),
"F16" => Some(egui::Key::F16),
"F17" => Some(egui::Key::F17),
"F18" => Some(egui::Key::F18),
"F19" => Some(egui::Key::F19),
"F20" => Some(egui::Key::F20),
_ => None,
}

View file

@ -4,26 +4,12 @@
pub mod backend;
mod events;
mod glow_wrapping;
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::*;
@ -83,6 +69,7 @@ pub fn system_theme() -> Option<Theme> {
}
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
use wasm_bindgen::JsCast;
let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
@ -90,7 +77,7 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
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))
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
@ -121,10 +108,6 @@ pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2
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;
@ -256,3 +239,47 @@ pub fn percent_decode(s: &str) -> String {
.decode_utf8_lossy()
.to_string()
}
// ----------------------------------------------------------------------------
pub(crate) 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

@ -15,7 +15,7 @@ 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);
*ctx.memory() = memory;
}
Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err);
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) {
match ctx.memory(|mem| ron::to_string(mem)) {
match ron::to_string(&*ctx.memory()) {
Ok(ron) => {
local_storage_set("egui_memory_ron", &ron);
}

View file

@ -10,6 +10,7 @@ use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.document()
@ -22,6 +23,7 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
/// Text event handler,
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
@ -127,6 +129,7 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;

10
egui-wgpu/CHANGELOG.md Normal file
View file

@ -0,0 +1,10 @@
# Changelog for egui-wgpu
All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased
* 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,18 +1,18 @@
[package]
name = "egui-wgpu"
version = "0.21.0"
version = "0.18.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>",
"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"
rust-version = "1.61"
homepage = "https://github.com/emilk/egui/tree/master/egui-wgpu"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
repository = "https://github.com/emilk/egui/tree/master/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
@ -28,29 +28,23 @@ 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"]
winit = ["dep:pollster", "dep:winit"]
[dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
"bytemuck",
egui = { version = "0.18.1", path = "../egui", 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"
wgpu = "0.13"
#! ### 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 }
pollster = { version = "0.2", optional = true }
winit = { version = "0.27.2", optional = true }

80
egui-wgpu/src/egui.wgsl Normal file
View file

@ -0,0 +1,80 @@
// Vertex shader bindings
struct VertexOutput {
@location(0) tex_coord: vec2<f32>,
@location(1) color: vec4<f32>,
@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 from 0-255
fn linear_from_srgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(10.31475);
let lower = srgb / vec3<f32>(3294.6);
let higher = pow((srgb + vec3<f32>(14.025)) / vec3<f32>(269.025), vec3<f32>(2.4));
return select(higher, lower, cutoff);
}
// [u8; 4] SRGB as u32 -> [r, g, b, a]
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),
);
}
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;
let color = unpack_color(a_color);
out.color = vec4<f32>(linear_from_srgb(color.rgb), color.a / 255.0);
out.position = position_from_screen(a_pos);
return out;
}
@vertex
fn vs_conv_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;
let color = unpack_color(a_color);
out.color = vec4<f32>(color.rgba / 255.0);
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(in: VertexOutput) -> @location(0) vec4<f32> {
return in.color * textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
}

20
egui-wgpu/src/lib.rs Normal file
View file

@ -0,0 +1,20 @@
//! 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`] on [`wgpu`].
pub mod renderer;
pub use renderer::CallbackFn;
/// Module for painting [`egui`] with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
pub mod winit;
#[cfg(feature = "winit")]
pub use crate::winit::RenderState;

View file

@ -1,47 +1,38 @@
#![allow(unsafe_code)]
use std::num::NonZeroU64;
use std::ops::Range;
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
use egui::{epaint::Primitive, NumExt, PaintCallbackInfo};
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
/// A callback function that can be used to compose an [`egui::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 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 the [`wgpu::RenderPass`] so that it
/// can issue draw commands.
///
/// 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`][crate::renderer::RenderPass::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.
/// See the [`custom3d_glow`](https://github.com/emilk/egui/blob/master/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 PrepareCallback = dyn Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send;
type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
@ -49,7 +40,7 @@ type PaintCallback =
impl Default for CallbackFn {
fn default() -> Self {
CallbackFn {
prepare: Box::new(|_, _, _, _| Vec::new()),
prepare: Box::new(|_, _, _| ()),
paint: Box::new(|_, _, _| ()),
}
}
@ -60,32 +51,10 @@ impl CallbackFn {
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.
/// Set the prepare callback
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,
F: Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send + 'static,
{
self.prepare = Box::new(prepare) as _;
self
@ -104,6 +73,14 @@ impl CallbackFn {
}
}
/// Enum for selecting the right buffer type.
#[derive(Debug)]
enum BufferType {
Uniform,
Index,
Vertex,
}
/// Information about the screen used for rendering.
pub struct ScreenDescriptor {
/// Size of the window in physical pixels.
@ -133,50 +110,43 @@ struct UniformBuffer {
_padding: [u32; 2],
}
struct SlicedBuffer {
/// Wraps the buffers and includes additional information.
#[derive(Debug)]
struct SizedBuffer {
buffer: wgpu::Buffer,
slices: Vec<Range<wgpu::BufferAddress>>,
capacity: wgpu::BufferAddress,
/// number of bytes
size: usize,
}
/// Renderer for a egui based GUI.
pub struct Renderer {
pipeline: wgpu::RenderPipeline,
index_buffer: SlicedBuffer,
vertex_buffer: SlicedBuffer,
uniform_buffer: wgpu::Buffer,
/// Render pass to render a egui based GUI.
pub struct RenderPass {
render_pipeline: wgpu::RenderPipeline,
index_buffers: Vec<SizedBuffer>,
vertex_buffers: Vec<SizedBuffer>,
uniform_buffer: SizedBuffer,
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)>,
textures: HashMap<egui::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
/// Storage for use by [`egui::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.
impl RenderPass {
/// Creates a new render pass to render a egui UI.
///
/// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
/// If the format passed is not a *Srgb format, the shader will automatically convert to `sRGB` colors in the shader.
pub fn new(
device: &wgpu::Device,
output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
output_format: wgpu::TextureFormat,
msaa_samples: u32,
) -> Self {
crate::profile_function!();
let shader = wgpu::ShaderModuleDescriptor {
label: Some("egui"),
label: Some("egui_shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
};
let module = device.create_shader_module(shader);
@ -189,6 +159,10 @@ impl Renderer {
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_buffer = SizedBuffer {
buffer: uniform_buffer,
size: std::mem::size_of::<UniformBuffer>(),
};
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@ -198,7 +172,7 @@ impl Renderer {
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
min_binding_size: None,
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
@ -211,7 +185,7 @@ impl Renderer {
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &uniform_buffer,
buffer: &uniform_buffer.buffer,
offset: 0,
size: None,
}),
@ -247,19 +221,15 @@ impl Renderer {
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 {
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("egui_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
entry_point: "vs_main",
entry_point: if output_format.describe().srgb {
"vs_main"
} else {
"vs_conv_main"
},
module: &module,
buffers: &[wgpu::VertexBufferLayout {
array_stride: 5 * 4,
@ -279,7 +249,7 @@ impl Renderer {
polygon_mode: wgpu::PolygonMode::default(),
strip_index_format: None,
},
depth_stencil,
depth_stencil: None,
multisample: wgpu::MultisampleState {
alpha_to_coverage_enabled: false,
count: msaa_samples,
@ -288,14 +258,9 @@ impl Renderer {
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
},
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: output_color_format,
format: output_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
@ -314,59 +279,77 @@ impl Renderer {
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,
},
render_pipeline,
vertex_buffers: Vec::with_capacity(64),
index_buffers: Vec::with_capacity(64),
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],
/// Executes the egui render pass.
pub fn execute(
&self,
encoder: &mut wgpu::CommandEncoder,
color_attachment: &wgpu::TextureView,
paint_jobs: &[egui::epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor,
clear_color: Option<wgpu::Color>,
) {
let load_operation = if let Some(color) = clear_color {
wgpu::LoadOp::Clear(color)
} else {
wgpu::LoadOp::Load
};
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_attachment,
resolve_target: None,
ops: wgpu::Operations {
load: load_operation,
store: true,
},
})],
depth_stencil_attachment: None,
label: Some("egui main render pass"),
});
rpass.push_debug_group("egui_pass");
self.execute_with_renderpass(&mut rpass, paint_jobs, screen_descriptor);
rpass.pop_debug_group();
}
/// Executes the egui render pass onto an existing wgpu renderpass.
pub fn execute_with_renderpass<'rpass>(
&'rpass self,
rpass: &mut wgpu::RenderPass<'rpass>,
paint_jobs: &[egui::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
// Whether or not we need to reset the renderpass state 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();
let mut index_buffers = self.index_buffers.iter();
let mut vertex_buffers = self.vertex_buffers.iter();
for epaint::ClippedPrimitive {
for egui::ClippedPrimitive {
clip_rect,
primitive,
} in paint_jobs
{
if needs_reset {
render_pass.set_viewport(
rpass.set_viewport(
0.0,
0.0,
size_in_pixels[0] as f32,
@ -374,8 +357,8 @@ impl Renderer {
0.0,
1.0,
);
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
rpass.set_pipeline(&self.render_pipeline);
rpass.set_bind_group(0, &self.uniform_bind_group, &[]);
needs_reset = false;
}
@ -383,34 +366,31 @@ impl Renderer {
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.
// Skip rendering with 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();
// If this is a mesh, we need to advance the index and vertex buffer iterators
index_buffers.next().unwrap();
vertex_buffers.next().unwrap();
}
continue;
}
render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
rpass.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();
let index_buffer = index_buffers.next().unwrap();
let vertex_buffer = vertex_buffers.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()),
rpass.set_bind_group(1, bind_group, &[]);
rpass.set_index_buffer(
index_buffer.buffer.slice(..),
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);
rpass.set_vertex_buffer(0, vertex_buffer.buffer.slice(..));
rpass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
} else {
tracing::warn!("Missing texture: {:?}", mesh.texture_id);
}
@ -424,28 +404,26 @@ impl Renderer {
};
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.
// Set the viewport rect
// Transform callback rect to physical pixels:
let rect_min_x = pixels_per_point * callback.rect.min.x;
let rect_min_y = pixels_per_point * callback.rect.min.y;
let rect_max_x = pixels_per_point * callback.rect.max.x;
let rect_max_y = pixels_per_point * callback.rect.max.y;
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
let rect_min_x = rect_min_x.round();
let rect_min_y = rect_min_y.round();
let rect_max_x = rect_max_x.round();
let rect_max_y = rect_max_y.round();
render_pass.set_viewport(
min.x,
min.y,
max.x - min.x,
max.y - min.y,
rpass.set_viewport(
rect_min_x,
rect_min_y,
rect_max_x - rect_min_x,
rect_max_y - rect_min_y,
0.0,
1.0,
);
@ -458,7 +436,7 @@ impl Renderer {
pixels_per_point,
screen_size_px: size_in_pixels,
},
render_pass,
rpass,
&self.paint_callback_resources,
);
}
@ -466,19 +444,17 @@ impl Renderer {
}
}
render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
rpass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
}
/// Should be called before `render()`.
/// Should be called before `execute()`.
pub fn update_texture(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
id: epaint::TextureId,
image_delta: &epaint::ImageDelta,
id: egui::TextureId,
image_delta: &egui::epaint::ImageDelta,
) {
crate::profile_function!();
let width = image_delta.image.width() as u32;
let height = image_delta.image.height() as u32;
@ -489,7 +465,7 @@ impl Renderer {
};
let data_color32 = match &image_delta.image {
epaint::ImageData::Color(image) => {
egui::ImageData::Color(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
@ -497,13 +473,13 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
egui::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<_>>())
Cow::Owned(image.srgba_pixels(1.0).collect::<Vec<_>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@ -543,25 +519,27 @@ impl Renderer {
);
} 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,
label: None,
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.
format: wgpu::TextureFormat::Rgba8UnormSrgb,
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 filter = match image_delta.filter {
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
};
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: None,
mag_filter: filter,
min_filter: filter,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
label: None,
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
@ -572,7 +550,7 @@ impl Renderer {
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
@ -582,7 +560,7 @@ impl Renderer {
};
}
pub fn free_texture(&mut self, id: &epaint::TextureId) {
pub fn free_texture(&mut self, id: &egui::TextureId) {
self.textures.remove(id);
}
@ -590,15 +568,15 @@ impl Renderer {
///
/// 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).
/// or [`egui::Context::load_texture`].
pub fn texture(
&self,
id: &epaint::TextureId,
id: &egui::TextureId,
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
self.textures.get(id)
}
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
/// Registers a `wgpu::Texture` with a `egui::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
@ -609,12 +587,18 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
texture_filter: wgpu::FilterMode,
) -> epaint::TextureId {
) -> egui::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()),
label: Some(
format!(
"egui_user_image_{}_texture_sampler",
self.next_user_texture_id
)
.as_str(),
),
mag_filter: texture_filter,
min_filter: texture_filter,
..Default::default()
@ -622,30 +606,7 @@ impl Renderer {
)
}
/// 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
/// Registers a `wgpu::Texture` with a `egui::TextureId` while also accepting custom
/// `wgpu::SamplerDescriptor` options.
///
/// This allows applications to specify individual minification/magnification filters as well as
@ -660,16 +621,20 @@ impl Renderer {
device: &wgpu::Device,
texture: &wgpu::TextureView,
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
) -> epaint::TextureId {
crate::profile_function!();
) -> egui::TextureId {
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()),
label: Some(
format!(
"egui_user_image_{}_texture_bind_group",
self.next_user_texture_id
)
.as_str(),
),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
@ -683,208 +648,124 @@ impl Renderer {
],
});
let id = epaint::TextureId::User(self.next_user_texture_id);
let id = egui::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.
/// Uploads the uniform, vertex and index data used by the render pass.
/// Should be called before `execute()`.
pub fn update_buffers(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
paint_jobs: &[epaint::ClippedPrimitive],
paint_jobs: &[egui::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(),
}]),
);
}
self.update_buffer(
device,
queue,
&BufferType::Uniform,
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() {
let mut mesh_idx = 0;
for egui::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 data: &[u8] = bytemuck::cast_slice(&mesh.indices);
if mesh_idx < self.index_buffers.len() {
self.update_buffer(device, queue, &BufferType::Index, mesh_idx, data);
} else {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("egui_index_buffer"),
contents: data,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
self.index_buffers.push(SizedBuffer {
buffer,
size: data.len(),
});
}
{
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),
);
let data: &[u8] = bytemuck::cast_slice(&mesh.vertices);
if mesh_idx < self.vertex_buffers.len() {
self.update_buffer(device, queue, &BufferType::Vertex, mesh_idx, data);
} else {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("egui_vertex_buffer"),
contents: data,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
self.vertex_buffers.push(SizedBuffer {
buffer,
size: data.len(),
});
}
mesh_idx += 1;
}
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`");
tracing::warn!("Unknown paint callback: expected `egui_gpu::CallbackFn`");
continue;
};
crate::profile_scope!("callback");
user_cmd_bufs.extend((cbfn.prepare)(
device,
queue,
encoder,
&mut self.paint_callback_resources,
));
(cbfn.prepare)(device, queue, &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()
})
}
/// Updates the buffers used by egui. Will properly re-size the buffers if needed.
fn update_buffer(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
buffer_type: &BufferType,
index: usize,
data: &[u8],
) {
let (buffer, storage, label) = match buffer_type {
BufferType::Index => (
&mut self.index_buffers[index],
wgpu::BufferUsages::INDEX,
"egui_index_buffer",
),
BufferType::Vertex => (
&mut self.vertex_buffers[index],
wgpu::BufferUsages::VERTEX,
"egui_vertex_buffer",
),
BufferType::Uniform => (
&mut self.uniform_buffer,
wgpu::BufferUsages::UNIFORM,
"egui_uniform_buffer",
),
};
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,
})
if data.len() > buffer.size {
buffer.size = data.len();
buffer.buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(data),
usage: storage | wgpu::BufferUsages::COPY_DST,
});
} else {
queue.write_buffer(&buffer.buffer, 0, data);
}
}
}
/// A Rect in physical pixel space, used for setting cliipping rectangles.
@ -896,7 +777,7 @@ struct ScissorRect {
}
impl ScissorRect {
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
fn new(clip_rect: &egui::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;
@ -915,17 +796,20 @@ impl ScissorRect {
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 {
let width = (clip_max_x - clip_min_x).at_least(1);
let height = (clip_max_y - clip_min_y).at_least(1);
ScissorRect {
x: clip_min_x,
y: clip_min_y,
width: clip_max_x - clip_min_x,
height: clip_max_y - clip_min_y,
width,
height,
}
}
}
#[test]
fn renderer_impl_send_sync() {
fn render_pass_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Renderer>();
assert_send_sync::<RenderPass>();
}

308
egui-wgpu/src/winit.rs Normal file
View file

@ -0,0 +1,308 @@
use std::sync::Arc;
use egui::mutex::RwLock;
use tracing::error;
use wgpu::{Adapter, Instance, Surface};
use crate::renderer;
/// Access to the render state for egui, which can be useful in combination with
/// [`egui::PaintCallback`]s for custom rendering using WGPU.
#[derive(Clone)]
pub struct RenderState {
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: wgpu::TextureFormat,
pub egui_rpass: Arc<RwLock<renderer::RenderPass>>,
}
struct SurfaceState {
surface: Surface,
width: u32,
height: u32,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
///
/// Alternatively you can use [`crate::renderer`] directly.
pub struct Painter<'a> {
power_preference: wgpu::PowerPreference,
device_descriptor: wgpu::DeviceDescriptor<'a>,
present_mode: wgpu::PresentMode,
msaa_samples: u32,
instance: Instance,
adapter: Option<Adapter>,
render_state: Option<RenderState>,
surface_state: Option<SurfaceState>,
}
impl<'a> Painter<'a> {
/// 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(
backends: wgpu::Backends,
power_preference: wgpu::PowerPreference,
device_descriptor: wgpu::DeviceDescriptor<'a>,
present_mode: wgpu::PresentMode,
msaa_samples: u32,
) -> Self {
let instance = wgpu::Instance::new(backends);
Self {
power_preference,
device_descriptor,
present_mode,
msaa_samples,
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.as_ref().cloned()
}
async fn init_render_state(
&self,
adapter: &Adapter,
target_format: wgpu::TextureFormat,
) -> RenderState {
let (device, queue) =
pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap();
let rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples);
RenderState {
device: Arc::new(device),
queue: Arc::new(queue),
target_format,
egui_rpass: Arc::new(RwLock::new(rpass)),
}
}
// 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.
fn ensure_render_state_for_surface(&mut self, surface: &Surface) {
self.adapter.get_or_insert_with(|| {
pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: self.power_preference,
compatible_surface: Some(surface),
force_fallback_adapter: false,
}))
.unwrap()
});
if self.render_state.is_none() {
let adapter = self.adapter.as_ref().unwrap();
let swapchain_format = surface.get_supported_formats(adapter)[0];
let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format));
self.render_state = Some(rs);
}
}
fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
let render_state = self
.render_state
.as_ref()
.expect("Render state should exist before surface configuration");
let format = render_state.target_format;
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: width_in_pixels,
height: height_in_pixels,
present_mode: self.present_mode,
};
let surface_state = self
.surface_state
.as_mut()
.expect("Surface state should exist before surface configuration");
surface_state
.surface
.configure(&render_state.device, &config);
surface_state.width = width_in_pixels;
surface_state.height = height_in_pixels;
}
/// 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`).
pub unsafe fn set_window(&mut self, window: Option<&winit::window::Window>) {
match window {
Some(window) => {
let surface = self.instance.create_surface(&window);
self.ensure_render_state_for_surface(&surface);
let size = window.inner_size();
let width = size.width;
let height = size.height;
self.surface_state = Some(SurfaceState {
surface,
width,
height,
});
self.configure_surface(width, height);
}
None => {
self.surface_state = None;
}
}
}
/// 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)
}
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
if self.surface_state.is_some() {
self.configure_surface(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: egui::Rgba,
clipped_primitives: &[egui::ClippedPrimitive],
textures_delta: &egui::TexturesDelta,
) {
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 = match surface_state.surface.get_current_texture() {
Ok(frame) => frame,
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"
return;
}
Err(e) => {
tracing::warn!("Dropped frame with error: {e}");
return;
}
};
let output_view = output_frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
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 mut rpass = render_state.egui_rpass.write();
for (id, image_delta) in &textures_delta.set {
rpass.update_texture(&render_state.device, &render_state.queue, *id, image_delta);
}
rpass.update_buffers(
&render_state.device,
&render_state.queue,
clipped_primitives,
&screen_descriptor,
);
}
// Record all render passes.
render_state.egui_rpass.read().execute(
&mut encoder,
&output_view,
clipped_primitives,
&screen_descriptor,
Some(wgpu::Color {
r: clear_color.r() as f64,
g: clear_color.g() as f64,
b: clear_color.b() as f64,
a: clear_color.a() as f64,
}),
);
{
let mut rpass = render_state.egui_rpass.write();
for id in &textures_delta.free {
rpass.free_texture(id);
}
}
// Submit the commands.
render_state.queue.submit(std::iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
}
#[allow(clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}
}

View file

@ -3,30 +3,6 @@ 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)).

View file

@ -1,14 +1,14 @@
[package]
name = "egui-winit"
version = "0.21.1"
version = "0.18.0"
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"
rust-version = "1.61"
homepage = "https://github.com/emilk/egui/tree/master/egui-winit"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
repository = "https://github.com/emilk/egui/tree/master/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
@ -18,10 +18,7 @@ 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"]
default = ["clipboard", "links"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
bytemuck = ["egui/bytemuck"]
@ -36,41 +33,36 @@ links = ["webbrowser"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
puffin = ["dep:puffin"]
## Experimental support for a screen reader.
screen_reader = ["tts"]
## 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 = [
egui = { version = "0.18.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
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 }
winit = "0.27.2"
#! ### 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 }
puffin = { version = "0.13", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.8.3", optional = true }
# feature screen_reader
tts = { version = "0.20", optional = true } # Can't use 0.22 due to compilation problems on linux: https://github.com/emilk/egui/runs/7170127089?check_suite_focus=true#step:5:713
webbrowser = { version = "0.7", 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"] }
arboard = { version = "2.1", optional = true, default-features = false }

View file

@ -30,7 +30,6 @@ impl Clipboard {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(),
#[cfg(all(
any(
target_os = "linux",
@ -42,7 +41,6 @@ impl Clipboard {
feature = "smithay-clipboard"
))]
smithay: init_smithay_clipboard(wayland_display),
clipboard: Default::default(),
}
}
@ -62,7 +60,7 @@ impl Clipboard {
return match clipboard.load() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("smithay paste error: {err}");
tracing::error!("Paste error: {}", err);
None
}
};
@ -73,7 +71,7 @@ impl Clipboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
Err(err) => {
tracing::error!("arboard paste error: {err}");
tracing::error!("Paste error: {}", err);
None
}
};
@ -101,7 +99,7 @@ impl Clipboard {
#[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}");
tracing::error!("Copy/Cut error: {}", err);
}
return;
}
@ -112,11 +110,10 @@ impl Clipboard {
#[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}");
tracing::error!("Failed to initialize clipboard: {}", err);
None
}
}
@ -136,11 +133,10 @@ 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");
tracing::error!("Cannot initialize smithay clipboard without a display handle!");
None
}
}

View file

@ -11,19 +11,24 @@
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;
pub mod screen_reader;
mod window_settings;
pub use window_settings::WindowSettings;
use winit::event_loop::EventLoopWindowTarget;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
use winit::platform::unix::EventLoopWindowTargetExtUnix;
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
window.scale_factor() as f32
@ -34,37 +39,18 @@ pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 {
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>,
current_cursor_icon: egui::CursorIcon,
/// What egui uses.
current_pixels_per_point: f32,
clipboard: clipboard::Clipboard,
screen_reader: screen_reader::ScreenReader,
/// If `true`, mouse inputs will be treated as touches.
/// Useful for debugging touch support in egui.
@ -76,12 +62,6 @@ pub struct State {
///
/// 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 {
@ -100,35 +80,17 @@ impl State {
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
current_cursor_icon: egui::CursorIcon::Default,
current_pixels_per_point: 1.0,
clipboard: clipboard::Clipboard::new(wayland_display),
screen_reader: screen_reader::ScreenReader::default(),
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) {
@ -190,63 +152,51 @@ impl State {
/// 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`].
///
/// Returns `true` if egui 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 want to first call this
/// and only when this returns `false` pass on the events to your game.
///
/// Note that egui uses `tab` to move focus between elements, so this will always return `true` for tabs.
pub fn on_event(
&mut self,
egui_ctx: &egui::Context,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
) -> bool {
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,
}
false
}
WindowEvent::MouseInput { state, button, .. } => {
self.on_mouse_button_input(*state, *button);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
egui_ctx.wants_pointer_input()
}
WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(*delta);
EventResponse {
repaint: true,
consumed: egui_ctx.wants_pointer_input(),
}
egui_ctx.wants_pointer_input()
}
WindowEvent::CursorMoved { position, .. } => {
self.on_cursor_moved(*position);
EventResponse {
repaint: true,
consumed: egui_ctx.is_using_pointer(),
}
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,
}
false
}
// WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO
WindowEvent::Touch(touch) => {
self.on_touch(touch);
let consumed = match touch.phase {
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) => {
@ -255,92 +205,37 @@ impl State {
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 {
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,
}
egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab)
}
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,
}
false
}
WindowEvent::HoveredFile(path) => {
self.egui_input.hovered_files.push(egui::HoveredFile {
path: Some(path.clone()),
..Default::default()
});
EventResponse {
repaint: true,
consumed: false,
}
false
}
WindowEvent::HoveredFileCancelled => {
self.egui_input.hovered_files.clear();
EventResponse {
repaint: true,
consumed: false,
}
false
}
WindowEvent::DroppedFile(path) => {
self.egui_input.hovered_files.clear();
@ -348,10 +243,7 @@ impl State {
path: Some(path.clone()),
..Default::default()
});
EventResponse {
repaint: true,
consumed: false,
}
false
}
WindowEvent::ModifiersChanged(state) => {
self.egui_input.modifiers.alt = state.alt();
@ -363,56 +255,15 @@ impl State {
} else {
state.ctrl()
};
EventResponse {
repaint: true,
consumed: false,
}
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(),
}
_ => {
// dbg!(event);
false
}
}
}
/// 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,
@ -599,7 +450,6 @@ impl State {
self.egui_input.events.push(egui::Event::Key {
key,
pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers,
});
}
@ -620,6 +470,11 @@ impl State {
egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput,
) {
if egui_ctx.options().screen_reader {
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::PlatformOutput {
cursor_icon,
open_url,
@ -627,8 +482,6 @@ impl State {
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
@ -645,35 +498,25 @@ impl State {
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.
// prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing
#[cfg(windows)]
if self.current_cursor_icon == cursor_icon {
return;
}
self.current_cursor_icon = cursor_icon;
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(cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
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);
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window {
window.set_cursor_icon(cursor_icon);
}
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
window.set_cursor_visible(false);
}
}
}
@ -757,11 +600,6 @@ fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option<egui:
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,
@ -873,7 +711,6 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
/// 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",
@ -882,7 +719,6 @@ fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_v
target_os = "openbsd"
))]
{
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
return _event_loop.wayland_display();
}

View file

@ -0,0 +1,49 @@
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

@ -0,0 +1,77 @@
/// 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 initialize_window(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
if !cfg!(target_os = "windows") {
// 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 window is hidden and very difficult to find.
// So we don't restore window positions on Windows.
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(|| winit::window::Fullscreen::Borderless(None)),
)
} else {
window
}
}
}

View file

@ -1,13 +1,13 @@
[package]
name = "egui"
version = "0.21.0"
version = "0.18.1"
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"
rust-version = "1.61"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "../../README.md"
readme = "../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
@ -52,26 +52,15 @@ mint = ["epaint/mint"]
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"]
serde = ["dep:serde", "epaint/serde"]
[dependencies]
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
epaint = { version = "0.18.1", 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",
] }
ahash = "0.8"
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 }
@ -79,6 +68,4 @@ 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",
] }
tracing = { version = "0.1", optional = true, default-features = false, features = ["std"] }

View file

@ -2,6 +2,6 @@ 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>.
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/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

@ -9,7 +9,6 @@ pub(crate) struct AnimationManager {
#[derive(Clone, Debug)]
struct BoolAnim {
value: bool,
/// when did `value` last toggle?
toggle_time: f64,
}
@ -17,9 +16,7 @@ struct BoolAnim {
#[derive(Clone, Debug)]
struct ValueAnim {
from_value: f32,
to_value: f32,
/// when did `value` last toggle?
toggle_time: f64,
}

View file

@ -2,17 +2,17 @@
//! It has no frame or own size. It is potentially movable.
//! It is the foundation for windows and popups.
use std::{fmt::Debug, hash::Hash};
use crate::*;
/// State that is persisted between frames.
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
// 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 known pos
pub pos: Pos2,
/// Last know size. Used for catching clicks.
pub size: Vec2,
@ -23,22 +23,8 @@ pub(crate) struct State {
}
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)
Rect::from_min_size(self.pos, self.size)
}
}
@ -62,27 +48,23 @@ pub struct Area {
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 {
pub fn new(id_source: impl Hash) -> Self {
Self {
id: id.into(),
id: Id::new(id_source),
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,
}
@ -147,24 +129,6 @@ impl Area {
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());
@ -205,15 +169,14 @@ impl Area {
pub(crate) struct Prepared {
layer_id: LayerId,
state: State,
move_response: Response,
pub(crate) movable: bool,
enabled: bool,
drag_bounds: Option<Rect>,
/// We always make windows invisible the first frame to hide "first-frame-jitters".
/// Set the first frame of new windows with anchors.
///
/// 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.
/// and then can correctly position the window the next frame,
/// without having one frame where the window is positioned in the wrong place.
temporarily_invisible: bool,
}
@ -239,95 +202,44 @@ impl Area {
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 state = ctx.memory().areas.get(id).cloned();
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,
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
size: Vec2::ZERO,
interactable,
});
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
state.pos = new_pos.unwrap_or(state.pos);
state.interactable = interactable;
let mut temporarily_invisible = false;
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
if is_new {
temporarily_invisible = true;
} 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,
);
let screen = ctx.available_rect();
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
}
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(),
);
}
state.pos = ctx.round_pos_to_pixels(state.pos);
Prepared {
layer_id,
state,
move_response,
movable,
enabled,
drag_bounds,
temporarily_invisible: is_new,
temporarily_invisible,
}
}
@ -345,7 +257,7 @@ impl Area {
}
let layer_id = LayerId::new(self.order, self.id);
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
let area_rect = ctx.memory().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);
@ -374,7 +286,7 @@ impl Prepared {
}
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.screen_rect();
let screen_rect = ctx.input().screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds {
bounds.intersect(screen_rect) // protect against infinite bounds
@ -390,16 +302,14 @@ impl Prepared {
};
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)),
self.state.pos,
bounds.max.at_least(self.state.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)
let clip_rect = Rect::from_min_max(self.state.pos, bounds.max)
.expand(clip_rect_margin)
.intersect(bounds);
@ -420,15 +330,50 @@ impl Prepared {
let Prepared {
layer_id,
mut state,
move_response,
enabled: _,
drag_bounds: _,
movable,
enabled,
drag_bounds,
temporarily_invisible: _,
} = self;
state.size = content_ui.min_rect().size();
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
let interact_id = layer_id.id.with("move");
let sense = if movable {
Sense::click_and_drag()
} else {
Sense::click() // allow clicks to bring to front
};
let move_response = ctx.interact(
Rect::EVERYTHING,
ctx.style().spacing.item_spacing,
layer_id,
interact_id,
state.rect(),
sense,
enabled,
);
if move_response.dragged() && movable {
state.pos += ctx.input().pointer.delta();
}
// Important check - don't try to move e.g. a combobox popup!
if movable {
state.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().areas.visible_last_frame(&layer_id)
{
ctx.memory().areas.move_to_top(layer_id);
ctx.request_repaint();
}
ctx.memory().areas.set_state(layer_id, state);
move_response
}
@ -436,7 +381,7 @@ impl Prepared {
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());
let any_pressed = ctx.input().pointer.any_pressed();
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
} else {
false
@ -444,13 +389,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
}
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()
});
let mut existing: Vec<Rect> = ctx
.memory()
.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();

View file

@ -26,14 +26,13 @@ pub struct CollapsingState {
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 })
})
ctx.data()
.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));
ctx.data().insert_persisted(self.id, self.state);
}
pub fn id(&self) -> Id {
@ -65,7 +64,7 @@ impl CollapsingState {
/// 0 for closed, 1 for open, with tweening
pub fn openness(&self, ctx: &Context) -> f32 {
if ctx.memory(|mem| mem.everything_is_visible()) {
if ctx.memory().everything_is_visible() {
1.0
} else {
ctx.animate_bool(self.id, self.state.open)
@ -112,7 +111,10 @@ impl CollapsingState {
response.rect.center().y,
));
let openness = self.openness(ui.ctx());
let small_icon_response = response.clone().with_new_rect(icon_rect);
let small_icon_response = Response {
rect: icon_rect,
..response.clone()
};
icon_fn(ui, openness, &small_icon_response);
response
}
@ -141,10 +143,9 @@ impl CollapsingState {
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;
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
(collapser, add_header(ui))
});
HeaderResponse {
@ -310,6 +311,7 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
/// 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 stroke = visuals.fg_stroke;
let rect = response.rect;
@ -323,11 +325,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
*p = rect.center() + rotation * (*p - rect.center());
}
ui.painter().add(Shape::convex_polygon(
points,
visuals.fg_stroke.color,
Stroke::NONE,
));
ui.painter().add(Shape::closed_line(points, stroke));
}
/// A function that paints an icon indicating if the region is open or not
@ -554,7 +552,7 @@ impl CollapsingHeader {
ui.painter().add(epaint::RectShape {
rect: header_response.rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
// stroke: Default::default(),
});
@ -574,7 +572,10 @@ impl CollapsingHeader {
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);
let icon_response = Response {
rect: icon_rect,
..header_response.clone()
};
if let Some(icon) = icon {
icon(ui, openness, &icon_response);
} else {
@ -598,23 +599,13 @@ impl CollapsingHeader {
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)
self.show_dyn(ui, Box::new(add_body))
}
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).
@ -627,11 +618,7 @@ impl CollapsingHeader {
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)
};
let ret_response = state.show_body_indented(&header_response, ui, add_body);
if let Some(ret_response) = ret_response {
CollapsingResponse {

View file

@ -1,16 +1,8 @@
use crate::{style::WidgetVisuals, *};
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)>;
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// A drop-down selection menu with a descriptive label.
///
@ -36,7 +28,6 @@ pub struct ComboBox {
selected_text: WidgetText,
width: Option<f32>,
icon: Option<IconPainter>,
wrap_enabled: bool,
}
impl ComboBox {
@ -48,7 +39,6 @@ impl ComboBox {
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
@ -61,7 +51,6 @@ impl ComboBox {
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
@ -73,11 +62,10 @@ impl ComboBox {
selected_text: Default::default(),
width: None,
icon: None,
wrap_enabled: false,
}
}
/// Set the outer width of the button and menu.
/// Set the width of the button and menu
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
@ -101,7 +89,6 @@ impl ComboBox {
/// rect: egui::Rect,
/// visuals: &egui::style::WidgetVisuals,
/// _is_open: bool,
/// _above_or_below: egui::AboveOrBelow,
/// ) {
/// let rect = egui::Rect::from_center_size(
/// rect.center(),
@ -120,20 +107,11 @@ impl ComboBox {
/// .show_ui(ui, |_ui| {});
/// # });
/// ```
pub fn icon(
mut self,
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
) -> Self {
pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + '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.
@ -156,21 +134,15 @@ impl ComboBox {
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(width) = width {
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
}
let mut ir = combo_box_dyn(ui, button_id, selected_text, menu_contents, icon);
if let Some(label) = label {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
@ -237,59 +209,18 @@ fn combo_box_dyn<'c, R>(
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 is_popup_open = ui.memory().is_popup_open(popup_id);
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 full_minimum_width = ui.spacing().slider_width;
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);
let galley = selected_text.into_galley(ui, Some(false), f32::INFINITY, 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 = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
let width = width.at_least(full_minimum_width);
let height = galley.size().y.max(icon_size.y);
@ -312,15 +243,9 @@ fn combo_box_dyn<'c, R>(
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,
);
paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals);
}
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
@ -329,20 +254,14 @@ fn combo_box_dyn<'c, R>(
});
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
ui.memory().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
},
);
let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| {
ScrollArea::vertical()
.max_height(ui.spacing().combo_height)
.show(ui, menu_contents)
.inner
});
InnerResponse {
inner,
@ -386,7 +305,7 @@ fn button_frame(
epaint::RectShape {
rect: outer_rect.expand(visuals.expansion),
rounding: visuals.rounding,
fill: visuals.weak_bg_fill,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
},
);
@ -397,33 +316,13 @@ fn button_frame(
response
}
fn paint_default_icon(
painter: &Painter,
rect: Rect,
visuals: &WidgetVisuals,
above_or_below: AboveOrBelow,
) {
fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
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,
));
}
}
painter.add(Shape::closed_line(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke,
));
}

View file

@ -19,16 +19,11 @@ use epaint::*;
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,
}
@ -47,18 +42,22 @@ impl Frame {
}
}
pub fn side_top_panel(style: &Style) -> Self {
pub(crate) fn side_top_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::symmetric(8.0, 2.0),
fill: style.visuals.panel_fill,
rounding: Rounding::none(),
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
..Default::default()
}
}
pub fn central_panel(style: &Style) -> Self {
pub(crate) fn central_panel(style: &Style) -> Self {
Self {
inner_margin: Margin::same(8.0),
fill: style.visuals.panel_fill,
rounding: Rounding::none(),
fill: style.visuals.window_fill(),
stroke: Default::default(),
..Default::default()
}
}
@ -76,8 +75,8 @@ impl Frame {
pub fn menu(style: &Style) -> Self {
Self {
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.menu_rounding,
inner_margin: Margin::same(1.0),
rounding: style.visuals.widgets.noninteractive.rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -87,8 +86,8 @@ impl Frame {
pub fn popup(style: &Style) -> Self {
Self {
inner_margin: style.spacing.menu_margin,
rounding: style.visuals.menu_rounding,
inner_margin: style.spacing.window_margin,
rounding: style.visuals.widgets.noninteractive.rounding,
shadow: style.visuals.popup_shadow,
fill: style.visuals.window_fill(),
stroke: style.visuals.window_stroke(),
@ -120,45 +119,38 @@ impl Frame {
}
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
@ -172,16 +164,6 @@ impl Frame {
}
}
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,
@ -262,13 +244,6 @@ impl Prepared {
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();
@ -283,6 +258,6 @@ impl Prepared {
ui.painter().set(where_to_put_background, shape);
}
ui.allocate_rect(self.content_with_margin(), Sense::hover())
ui.allocate_rect(paint_rect, Sense::hover())
}
}

View file

@ -9,7 +9,7 @@ pub(crate) mod frame;
pub mod panel;
pub mod popup;
pub(crate) mod resize;
pub mod scroll_area;
pub(crate) mod scroll_area;
pub(crate) mod window;
pub use {

View file

@ -0,0 +1,716 @@
//! Panels are [`Ui`] regions taking up e.g. the left side of a [`Ui`] or screen.
//!
//! Panels can either be a child of a [`Ui`] (taking up a portion of the parent)
//! or be top-level (taking up a portion of the whole screen).
//!
//! Together with [`Window`] and [`Area`]:s, top-level panels are
//! the only places where you can put you widgets.
//!
//! The order in which you add panels matter!
//! The first panel you add will always be the outermost, and the last you add will always be the innermost.
//!
//! You must never open one top-level panel from within another panel. Add one panel, then the next.
//!
//! ⚠ Always add any [`CentralPanel`] last.
//!
//! Add your [`Window`]:s after any top-level panels.
use std::ops::RangeInclusive;
use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct PanelState {
rect: Rect,
}
impl PanelState {
fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
ctx.data().get_persisted(bar_id)
}
fn store(self, ctx: &Context, bar_id: Id) {
ctx.data().insert_persisted(bar_id, self);
}
}
// ----------------------------------------------------------------------------
/// [`Left`](Side::Left) or [`Right`](Side::Right)
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Side {
Left,
Right,
}
impl Side {
fn opposite(self) -> Self {
match self {
Side::Left => Self::Right,
Side::Right => Self::Left,
}
}
fn set_rect_width(self, rect: &mut Rect, width: f32) {
match self {
Side::Left => rect.max.x = rect.min.x + width,
Side::Right => rect.min.x = rect.max.x - width,
}
}
fn side_x(self, rect: Rect) -> f32 {
match self {
Side::Left => rect.left(),
Side::Right => rect.right(),
}
}
}
/// A panel that covers the entire left or right side of a [`Ui`] or screen.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ Always add any [`CentralPanel`] last.
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::SidePanel::left("my_left_panel").show(ctx, |ui| {
/// ui.label("Hello World!");
/// });
/// # });
/// ```
///
/// See also [`TopBottomPanel`].
#[must_use = "You should call .show()"]
pub struct SidePanel {
side: Side,
id: Id,
frame: Option<Frame>,
resizable: bool,
default_width: f32,
width_range: RangeInclusive<f32>,
}
impl SidePanel {
/// `id_source`: Something unique, e.g. `"my_left_panel"`.
pub fn left(id_source: impl std::hash::Hash) -> Self {
Self::new(Side::Left, id_source)
}
/// `id_source`: Something unique, e.g. `"my_right_panel"`.
pub fn right(id_source: impl std::hash::Hash) -> Self {
Self::new(Side::Right, id_source)
}
/// `id_source`: Something unique, e.g. `"my_panel"`.
pub fn new(side: Side, id_source: impl std::hash::Hash) -> Self {
Self {
side,
id: Id::new(id_source),
frame: None,
resizable: true,
default_width: 200.0,
width_range: 96.0..=f32::INFINITY,
}
}
/// Can panel be resized by dragging the edge of it?
///
/// Default is `true`.
///
/// If you want your panel to be resizable you also need a widget in it that
/// takes up more space as you resize it, such as:
/// * Wrapping text ([`Ui::horizontal_wrapped`]).
/// * A [`ScrollArea`].
/// * A [`Separator`].
/// * A [`TextEdit`].
/// * …
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
/// The initial wrapping width of the [`SidePanel`].
pub fn default_width(mut self, default_width: f32) -> Self {
self.default_width = default_width;
self
}
pub fn min_width(mut self, min_width: f32) -> Self {
self.width_range = min_width..=(*self.width_range.end());
self
}
pub fn max_width(mut self, max_width: f32) -> Self {
self.width_range = (*self.width_range.start())..=max_width;
self
}
/// The allowable width range for resizable panels.
pub fn width_range(mut self, width_range: RangeInclusive<f32>) -> Self {
self.width_range = width_range;
self
}
/// Change the background color, margins, etc.
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl SidePanel {
/// Show the panel inside a [`Ui`].
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
/// Show the panel inside a [`Ui`].
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self {
side,
id,
frame,
resizable,
default_width,
width_range,
} = self;
let available_rect = ui.available_rect_before_wrap();
let mut panel_rect = available_rect;
{
let mut width = default_width;
if let Some(state) = PanelState::load(ui.ctx(), id) {
width = state.rect.width();
}
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
}
let mut resize_hover = false;
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
let resize_x = side.opposite().side_x(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.y_range().contains(&pointer.y)
&& (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().set_dragged_id(resize_id);
}
is_resizing = ui.memory().is_being_dragged(resize_id);
if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs();
let width =
clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
}
}
}
let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id);
panel_ui.expand_to_include_rect(panel_rect);
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
ui.set_min_width(*width_range.start());
add_contents(ui)
});
let rect = inner_response.response.rect;
{
let mut cursor = ui.cursor();
match side {
Side::Left => {
cursor.min.x = rect.max.x + ui.spacing().item_spacing.x;
}
Side::Right => {
cursor.max.x = rect.min.x - ui.spacing().item_spacing.x;
}
}
ui.set_cursor(cursor);
}
ui.expand_to_include_rect(rect);
PanelState { rect }.store(ui.ctx(), id);
if resize_hover || is_resizing {
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else {
ui.style().visuals.widgets.hovered.bg_stroke
};
// draw on top of ALL panels so that the resize line won't be covered by subsequent panels
let resize_layer = LayerId::new(Order::Foreground, Id::new("panel_resize"));
let resize_x = side.opposite().side_x(rect);
ui.ctx()
.layer_painter(resize_layer)
.vline(resize_x, rect.y_range(), stroke);
}
inner_response
}
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
let side = self.side;
let available_rect = ctx.available_rect();
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
Side::Left => ctx
.frame_state()
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
Side::Right => ctx
.frame_state()
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
}
inner_response
}
}
// ----------------------------------------------------------------------------
/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom)
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TopBottomSide {
Top,
Bottom,
}
impl TopBottomSide {
fn opposite(self) -> Self {
match self {
TopBottomSide::Top => Self::Bottom,
TopBottomSide::Bottom => Self::Top,
}
}
fn set_rect_height(self, rect: &mut Rect, height: f32) {
match self {
TopBottomSide::Top => rect.max.y = rect.min.y + height,
TopBottomSide::Bottom => rect.min.y = rect.max.y - height,
}
}
fn side_y(self, rect: Rect) -> f32 {
match self {
TopBottomSide::Top => rect.top(),
TopBottomSide::Bottom => rect.bottom(),
}
}
}
/// A panel that covers the entire top or bottom of a [`Ui`] or screen.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ Always add any [`CentralPanel`] last.
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
/// ui.label("Hello World!");
/// });
/// # });
/// ```
///
/// See also [`SidePanel`].
#[must_use = "You should call .show()"]
pub struct TopBottomPanel {
side: TopBottomSide,
id: Id,
frame: Option<Frame>,
resizable: bool,
default_height: Option<f32>,
height_range: RangeInclusive<f32>,
}
impl TopBottomPanel {
/// `id_source`: Something unique, e.g. `"my_top_panel"`.
pub fn top(id_source: impl std::hash::Hash) -> Self {
Self::new(TopBottomSide::Top, id_source)
}
/// `id_source`: Something unique, e.g. `"my_bottom_panel"`.
pub fn bottom(id_source: impl std::hash::Hash) -> Self {
Self::new(TopBottomSide::Bottom, id_source)
}
/// `id_source`: Something unique, e.g. `"my_panel"`.
pub fn new(side: TopBottomSide, id_source: impl std::hash::Hash) -> Self {
Self {
side,
id: Id::new(id_source),
frame: None,
resizable: false,
default_height: None,
height_range: 20.0..=f32::INFINITY,
}
}
/// Can panel be resized by dragging the edge of it?
///
/// Default is `false`.
///
/// If you want your panel to be resizable you also need a widget in it that
/// takes up more space as you resize it, such as:
/// * Wrapping text ([`Ui::horizontal_wrapped`]).
/// * A [`ScrollArea`].
/// * A [`Separator`].
/// * A [`TextEdit`].
/// * …
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
/// The initial height of the [`SidePanel`].
/// Defaults to [`style::Spacing::interact_size`].y.
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_height = Some(default_height);
self
}
pub fn min_height(mut self, min_height: f32) -> Self {
self.height_range = min_height..=(*self.height_range.end());
self
}
pub fn max_height(mut self, max_height: f32) -> Self {
self.height_range = (*self.height_range.start())..=max_height;
self
}
/// The allowable height range for resizable panels.
pub fn height_range(mut self, height_range: RangeInclusive<f32>) -> Self {
self.height_range = height_range;
self
}
/// Change the background color, margins, etc.
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl TopBottomPanel {
/// Show the panel inside a [`Ui`].
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
/// Show the panel inside a [`Ui`].
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self {
side,
id,
frame,
resizable,
default_height,
height_range,
} = self;
let available_rect = ui.available_rect_before_wrap();
let mut panel_rect = available_rect;
{
let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) {
state.rect.height()
} else {
default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y)
};
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
}
let mut resize_hover = false;
let mut is_resizing = false;
if resizable {
let resize_id = id.with("__resize");
let latest_pos = ui.input().pointer.latest_pos();
if let Some(pointer) = latest_pos {
let we_are_on_top = ui
.ctx()
.layer_id_at(pointer)
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
let resize_y = side.opposite().side_y(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.x_range().contains(&pointer.x)
&& (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side;
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().interaction.drag_id = Some(resize_id);
}
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone())
.at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = CursorIcon::ResizeVertical;
}
}
}
let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id);
panel_ui.expand_to_include_rect(panel_rect);
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
ui.set_min_height(*height_range.start());
add_contents(ui)
});
let rect = inner_response.response.rect;
{
let mut cursor = ui.cursor();
match side {
TopBottomSide::Top => {
cursor.min.y = rect.max.y + ui.spacing().item_spacing.y;
}
TopBottomSide::Bottom => {
cursor.max.y = rect.min.y - ui.spacing().item_spacing.y;
}
}
ui.set_cursor(cursor);
}
ui.expand_to_include_rect(rect);
PanelState { rect }.store(ui.ctx(), id);
if resize_hover || is_resizing {
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else {
ui.style().visuals.widgets.hovered.bg_stroke
};
// draw on top of ALL panels so that the resize line won't be covered by subsequent panels
let resize_layer = LayerId::new(Order::Foreground, Id::new("panel_resize"));
let resize_y = side.opposite().side_y(rect);
ui.ctx()
.layer_painter(resize_layer)
.hline(rect.x_range(), resize_y, stroke);
}
inner_response
}
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let layer_id = LayerId::background();
let available_rect = ctx.available_rect();
let side = self.side;
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
TopBottomSide::Top => {
ctx.frame_state()
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
}
TopBottomSide::Bottom => {
ctx.frame_state()
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
}
}
inner_response
}
}
// ----------------------------------------------------------------------------
/// A panel that covers the remainder of the screen,
/// i.e. whatever area is left after adding other panels.
///
/// The order in which you add panels matter!
/// The first panel you add will always be the outermost, and the last you add will always be the innermost.
///
/// ⚠ [`CentralPanel`] must be added after all other panels!
///
/// NOTE: Any [`Window`]s and [`Area`]s will cover the top-level [`CentralPanel`].
///
/// See the [module level docs](crate::containers::panel) for more details.
///
/// ```
/// # egui::__run_test_ctx(|ctx| {
/// egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
/// ui.label("Hello World! From `TopBottomPanel`, that must be before `CentralPanel`!");
/// });
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.label("Hello World!");
/// });
/// # });
/// ```
#[must_use = "You should call .show()"]
#[derive(Default)]
pub struct CentralPanel {
frame: Option<Frame>,
}
impl CentralPanel {
/// Change the background color, margins, etc.
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
}
impl CentralPanel {
/// Show the panel inside a [`Ui`].
pub fn show_inside<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_inside_dyn(ui, Box::new(add_contents))
}
/// Show the panel inside a [`Ui`].
fn show_inside_dyn<'c, R>(
self,
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let Self { frame } = self;
let panel_rect = ui.available_rect_before_wrap();
let mut panel_ui = ui.child_ui(panel_rect, Layout::top_down(Align::Min));
let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
frame.show(&mut panel_ui, |ui| {
ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all
add_contents(ui)
})
}
/// Show the panel at the top level.
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
let available_rect = ctx.available_rect();
let layer_id = LayerId::background();
let id = Id::new("central_panel");
let clip_rect = ctx.input().screen_rect();
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.frame_state()
.allocate_central_panel(inner_response.response.rect);
inner_response
}
}
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 {
x.clamp(
range.start().min(*range.end()),
range.start().max(*range.end()),
)
}

View file

@ -6,42 +6,44 @@ 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)>,
pub(crate) struct MonoState {
last_id: Option<Id>,
last_size: Vec<Vec2>,
}
impl TooltipState {
pub fn load(ctx: &Context) -> Option<Self> {
ctx.data_mut(|d| d.get_temp(Id::null()))
impl MonoState {
fn load(ctx: &Context) -> Option<Self> {
ctx.data().get_temp(Id::null())
}
fn store(self, ctx: &Context) {
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
ctx.data().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)
fn tooltip_size(&self, id: Id, index: usize) -> Option<Vec2> {
if self.last_id == Some(id) {
self.last_size.get(index).cloned()
} 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();
fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) {
if self.last_id == Some(id) {
if let Some(stored_size) = self.last_size.get_mut(index) {
*stored_size = size;
} else {
self.last_size
.extend((0..index - self.last_size.len()).map(|_| Vec2::ZERO));
self.last_size.push(size);
}
return;
}
self.individual_ids_and_sizes
.insert(index, (individual_id, size));
self.last_id = Some(id);
self.last_size.clear();
self.last_size.extend((0..index).map(|_| Vec2::ZERO));
self.last_size.push(size);
}
}
@ -95,7 +97,9 @@ pub fn show_tooltip_at_pointer<R>(
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let suggested_pos = ctx
.input(|i| i.pointer.hover_pos())
.input()
.pointer
.hover_pos()
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
show_tooltip_at(ctx, id, suggested_pos, add_contents)
}
@ -110,7 +114,7 @@ pub fn show_tooltip_for<R>(
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()) {
let (above, position) = if ctx.input().any_touches() {
(true, expanded_rect.left_top())
} else {
(false, expanded_rect.left_bottom())
@ -147,84 +151,74 @@ pub fn show_tooltip_at<R>(
fn show_tooltip_at_avoid_dyn<'c, R>(
ctx: &Context,
individual_id: Id,
mut 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;
let mut tooltip_rect = Rect::NOTHING;
let mut count = 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 stored = ctx.frame_state().tooltip_rect;
let mut position = if frame_state.rect.is_positive() {
avoid_rect = avoid_rect.union(frame_state.rect);
let mut position = if let Some(stored) = stored {
// if there are multiple tooltips open they should use the same id for the `tooltip_size` caching to work.
id = stored.id;
tooltip_rect = stored.rect;
count = stored.count;
avoid_rect = avoid_rect.union(tooltip_rect);
if above {
frame_state.rect.left_top() - spacing * Vec2::Y
tooltip_rect.left_top()
} else {
frame_state.rect.left_bottom() + spacing * Vec2::Y
tooltip_rect.left_bottom()
}
} else if let Some(position) = suggested_position {
position
} else if ctx.memory(|mem| mem.everything_is_visible()) {
} else if ctx.memory().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 mut state = MonoState::load(ctx).unwrap_or_default();
let expected_size = state.tooltip_size(id, 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);
position = position.at_most(ctx.input().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
// Note: We do not use Rect::intersects() since it returns true even if the rects only touch.
if new_rect.shrink(1.0).intersects(avoid_rect) {
if above {
// place below instead:
position = avoid_rect.left_bottom() + spacing * Vec2::Y;
position = avoid_rect.left_bottom();
} else {
// place above instead:
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y);
}
}
}
let position = position.at_least(ctx.screen_rect().min);
let position = position.at_least(ctx.input().screen_rect().min);
let area_id = frame_state.common_id.with(frame_state.count);
let InnerResponse { inner, response } = show_tooltip_area_dyn(ctx, id, position, add_contents);
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));
state.set_tooltip_size(id, count, response.rect.size());
state.store(ctx);
ctx.frame_state().tooltip_rect = Some(crate::frame_state::TooltipRect {
id,
rect: tooltip_rect.union(response.rect),
count: count + 1,
});
Some(inner)
}
@ -252,17 +246,16 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) ->
/// Show a pop-over window.
fn show_tooltip_area_dyn<'c, R>(
ctx: &Context,
area_id: Id,
id: Id,
window_pos: Pos2,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
use containers::*;
Area::new(area_id)
Area::new(id)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.constrain(true)
.interactable(false)
.drag_bounds(ctx.screen_rect())
.drag_bounds(Rect::EVERYTHING) // disable clip rect
.show(ctx, |ui| {
Frame::popup(&ctx.style())
.show(ui, |ui| {
@ -273,47 +266,10 @@ fn show_tooltip_area_dyn<'c, R>(
})
}
/// 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.
/// Shows a popup 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.
@ -323,39 +279,30 @@ pub fn popup_below_widget<R>(
/// 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));
/// ui.memory().toggle_popup(popup_id);
/// }
/// let below = egui::AboveOrBelow::Below;
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
/// egui::popup::popup_below_widget(ui, popup_id, &response, |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>(
pub fn popup_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),
};
if ui.memory().is_popup_open(popup_id) {
let inner = Area::new(popup_id)
.order(Order::Foreground)
.constrain(true)
.fixed_pos(pos)
.pivot(pivot)
.fixed_pos(widget_response.rect.left_bottom())
.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();
let frame_margin = frame.inner_margin + frame.outer_margin;
frame
.show(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| {
@ -368,8 +315,8 @@ pub fn popup_above_or_below_widget<R>(
})
.inner;
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
ui.memory_mut(|mem| mem.close_popup());
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
ui.memory().close_popup();
}
Some(inner)
} else {

View file

@ -18,11 +18,11 @@ pub(crate) struct State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| d.get_persisted(id))
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_persisted(id, self));
ctx.data().insert_persisted(id, self);
}
}
@ -180,7 +180,7 @@ impl Resize {
.at_least(self.min_size)
.at_most(self.max_size)
.at_most(
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
);
State {
@ -305,7 +305,7 @@ impl Resize {
paint_resize_corner(ui, &corner_response);
if corner_response.hovered() || corner_response.dragged() {
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
}
}

View file

@ -14,15 +14,11 @@ 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,
pub 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],
@ -38,7 +34,6 @@ impl Default for State {
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],
@ -48,11 +43,11 @@ impl Default for State {
impl State {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| d.get_persisted(id))
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_persisted(id, self));
ctx.data().insert_persisted(id, self);
}
}
@ -66,10 +61,6 @@ pub struct ScrollAreaOutput<R> {
/// 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,
}
@ -97,10 +88,8 @@ pub struct ScrollArea {
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
@ -143,7 +132,6 @@ impl ScrollArea {
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
stick_to_end: [false; 2],
}
}
@ -205,8 +193,6 @@ impl ScrollArea {
/// 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)
@ -218,8 +204,6 @@ impl ScrollArea {
/// 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 {
@ -229,8 +213,6 @@ impl ScrollArea {
/// 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 {
@ -256,13 +238,12 @@ impl ScrollArea {
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.
/// 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.
/// is inputing text in a [`TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
pub fn enable_scrolling(mut self, enable: bool) -> Self {
@ -270,22 +251,10 @@ impl ScrollArea {
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.
/// 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 {
@ -351,7 +320,6 @@ impl ScrollArea {
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
stick_to_end,
} = self;
@ -426,75 +394,18 @@ impl ScrollArea {
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];
}
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
// Nice handling of forced resizing beyond the possible:
for d in 0..2 {
if !has_bar[d] {
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);
}
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,
@ -549,13 +460,9 @@ impl ScrollArea {
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 min_row = (viewport.min.y / row_height_with_spacing).floor() as usize;
let max_row = (viewport.max.y / row_height_with_spacing).ceil() as usize + 1;
let max_row = max_row.at_most(total_rows);
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;
@ -572,7 +479,7 @@ impl ScrollArea {
/// 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.
/// `add_contents` is past the viewport, 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,
@ -591,20 +498,18 @@ impl ScrollArea {
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);
let 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) {
fn end(self, ui: &mut Ui) -> State {
let Prepared {
id,
mut state,
@ -624,9 +529,7 @@ impl Prepared {
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());
let scroll_target = content_ui.ctx().frame_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();
@ -677,7 +580,23 @@ impl Prepared {
};
}
Rect::from_min_size(inner_rect.min, inner_size)
let mut inner_rect = Rect::from_min_size(inner_rect.min, inner_size);
// The window that egui sits in can't be expanded by egui, so we need to respect it:
for d in 0..2 {
if !has_bar[d] {
// HACK for when we have a vertical-only scroll area in a top level panel,
// and that panel is not wide enough for the contents.
// This code ensures we still see the scroll bar!
let max = ui.input().screen_rect().max[d]
- current_bar_use[d]
- ui.spacing().item_spacing[d];
inner_rect.max[d] = inner_rect.max[d].at_most(max);
// TODO(emilk): maybe auto-enable horizontal/vertical scrolling if this limit is reached
}
}
inner_rect
};
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
@ -687,11 +606,49 @@ impl Prepared {
content_size.y > inner_rect.height(),
];
if content_is_too_large[0] || content_is_too_large[1] {
// Drag contents to scroll (for touch screens mostly):
let sense = if self.scrolling_enabled {
Sense::drag()
} else {
Sense::hover()
};
let content_response = ui.interact(inner_rect, id.with("area"), sense);
if content_response.dragged() {
for d in 0..2 {
if has_bar[d] {
state.offset[d] -= ui.input().pointer.delta()[d];
state.vel[d] = ui.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().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();
}
}
}
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 mut frame_state = ui.ctx().frame_state();
let scroll_delta = frame_state.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;
@ -699,7 +656,7 @@ impl Prepared {
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);
frame_state.scroll_delta[d] = 0.0;
state.scroll_stuck_to_end[d] = false;
}
}
@ -728,29 +685,13 @@ impl Prepared {
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)
// margin between contents and scroll bar
let margin = animation_t * ui.spacing().item_spacing.x;
let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1)
let max_cross = outer_rect.max[1 - d]; // 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),
@ -841,7 +782,7 @@ impl Prepared {
),
)
};
let min_handle_size = ui.spacing().scroll_handle_min_length;
let min_handle_size = ui.spacing().scroll_bar_width;
if handle_rect.size()[d] < min_handle_size {
handle_rect = Rect::from_center_size(
handle_rect.center(),
@ -896,17 +837,14 @@ impl Prepared {
];
state.show_scroll = show_scroll_this_frame;
state.content_is_too_large = content_is_too_large;
state.store(ui.ctx(), id);
(content_size, state)
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
ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width
}

View file

@ -10,7 +10,7 @@ use super::*;
///
/// You can customize:
/// * title
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
/// * default, minimum, maximum and/or fixed size
/// * if the window has a scroll area (off by default)
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
/// * if there should be a close button (none by default)
@ -30,7 +30,6 @@ pub struct Window<'open> {
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
default_open: bool,
with_title_bar: bool,
}
@ -40,7 +39,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text()));
let area = Area::new(title.text());
Self {
title,
open: None,
@ -52,7 +51,6 @@ impl<'open> Window<'open> {
.default_size([340.0, 420.0]), // Default inner size of a window
scroll: ScrollArea::neither(),
collapsible: true,
default_open: true,
with_title_bar: true,
}
}
@ -79,27 +77,15 @@ impl<'open> Window<'open> {
self
}
/// If `false` the window will be non-interactive.
pub fn interactable(mut self, interactable: bool) -> Self {
self.area = self.area.interactable(interactable);
self
}
/// If `false` the window will be immovable.
pub fn movable(mut self, movable: bool) -> Self {
self.area = self.area.movable(movable);
self
}
/// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
// TODO(emilk): I'm not sure this is a good interface for this.
/// Not sure this is a good interface for this.
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
mutate(&mut self);
self
}
/// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))`
// TODO(emilk): I'm not sure this is a good interface for this.
/// Not sure this is a good interface for this.
pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self {
self.resize = mutate(self.resize);
self
@ -136,30 +122,6 @@ impl<'open> Window<'open> {
self
}
/// Sets the window position and prevents it from being dragged around.
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
self.area = self.area.fixed_pos(pos);
self
}
/// Constrains this window to the screen bounds.
pub fn constrain(mut self, constrain: bool) -> Self {
self.area = self.area.constrain(constrain);
self
}
/// Where the "root" of the window 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 window.
///
/// Default: [`Align2::LEFT_TOP`].
pub fn pivot(mut self, pivot: Align2) -> Self {
self.area = self.area.pivot(pivot);
self
}
/// Set anchor and distance.
///
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
@ -176,12 +138,6 @@ impl<'open> Window<'open> {
self
}
/// Set initial collapsed state of the window
pub fn default_open(mut self, default_open: bool) -> Self {
self.default_open = default_open;
self
}
/// Set initial size of the window.
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.default_size(default_size);
@ -200,17 +156,23 @@ impl<'open> Window<'open> {
self
}
/// Set initial position and size of the window.
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
/// Sets the window position and prevents it from being dragged around.
pub fn fixed_pos(mut self, pos: impl Into<Pos2>) -> Self {
self.area = self.area.fixed_pos(pos);
self
}
/// Sets the window size and prevents it from being resized by dragging its edges.
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size);
self
}
/// Set initial position and size of the window.
pub fn default_rect(self, rect: Rect) -> Self {
self.default_pos(rect.min).default_size(rect.size())
}
/// Sets the window pos and size and prevents it from being moved and resized by dragging its edges.
pub fn fixed_rect(self, rect: Rect) -> Self {
self.fixed_pos(rect.min).fixed_size(rect.size())
@ -295,14 +257,12 @@ impl<'open> Window<'open> {
resize,
scroll,
collapsible,
default_open,
with_title_bar,
} = self;
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
area.show_open_close_animation(ctx, &frame, is_open);
if !is_open {
@ -313,7 +273,7 @@ impl<'open> Window<'open> {
let area_layer_id = area.layer();
let resize_id = area_id.with("resize");
let mut collapsing =
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
let is_collapsed = with_title_bar && !collapsing.is_open();
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
@ -340,7 +300,7 @@ impl<'open> Window<'open> {
// Calculate roughly how much larger the window size is compared to the inner rect
let title_bar_height = if with_title_bar {
let style = ctx.style();
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
title.font_height(&ctx.fonts(), &style) + title_content_spacing
} else {
0.0
};
@ -426,7 +386,7 @@ impl<'open> Window<'open> {
ctx.style().visuals.widgets.active,
);
} else if let Some(hover_interaction) = hover_interaction {
if ctx.input(|i| i.pointer.has_pointer()) {
if ctx.input().pointer.has_pointer() {
paint_frame_interaction(
&mut area_content_ui,
outer_rect,
@ -438,12 +398,9 @@ impl<'open> Window<'open> {
content_inner
};
{
let pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
.left_top();
area.state_mut().set_left_top_pos(pos);
}
area.state_mut().pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
.min;
let full_response = area.end(ctx, area_content_ui);
@ -524,13 +481,13 @@ pub(crate) struct WindowInteraction {
impl WindowInteraction {
pub fn set_cursor(&self, ctx: &Context) {
if (self.left && self.top) || (self.right && self.bottom) {
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
} else if (self.right && self.top) || (self.left && self.bottom) {
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
} else if self.left || self.right {
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
} else if self.bottom || self.top {
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
}
}
@ -553,7 +510,7 @@ fn interact(
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds());
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
area.state_mut().set_left_top_pos(new_rect.left_top());
area.state_mut().pos = new_rect.min;
if window_interaction.is_resize() {
if let Some(mut state) = resize::State::load(ctx, resize_id) {
@ -562,7 +519,7 @@ fn interact(
}
}
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
ctx.memory().areas.move_to_top(area_layer_id);
Some(window_interaction)
}
@ -570,11 +527,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
window_interaction.set_cursor(ctx);
// Only move/resize windows with primary mouse button:
if !ctx.input(|i| i.pointer.primary_down()) {
if !ctx.input().pointer.primary_down() {
return None;
}
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
let pointer_pos = ctx.input().pointer.interact_pos()?;
let mut rect = window_interaction.start_rect; // prevent drift
if window_interaction.is_resize() {
@ -596,8 +553,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
// but we want anything interactive in the window (e.g. slider) to steal
// the drag from us. It is therefor important not to move the window the first frame,
// but instead let other widgets to the steal. HACK.
if !ctx.input(|i| i.pointer.any_pressed()) {
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
if !ctx.input().pointer.any_pressed() {
let press_origin = ctx.input().pointer.press_origin()?;
let delta = pointer_pos - press_origin;
rect = rect.translate(delta);
}
@ -615,31 +572,29 @@ fn window_interaction(
rect: Rect,
) -> Option<WindowInteraction> {
{
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
let drag_id = ctx.memory().interaction.drag_id;
if drag_id.is_some() && drag_id != Some(id) {
return None;
}
}
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
let mut window_interaction = { ctx.memory().window_interaction };
if window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx);
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
ctx.memory_mut(|mem| {
mem.interaction.drag_id = Some(id);
mem.interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction);
mem.window_interaction = window_interaction;
});
if ctx.input().pointer.any_pressed() && ctx.input().pointer.primary_down() {
ctx.memory().interaction.drag_id = Some(id);
ctx.memory().interaction.drag_is_window = true;
window_interaction = Some(hover_window_interaction);
ctx.memory().window_interaction = window_interaction;
}
}
}
if let Some(window_interaction) = window_interaction {
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
let is_active = ctx.memory().interaction.drag_id == Some(id);
if is_active && window_interaction.area_layer_id == area_layer_id {
return Some(window_interaction);
@ -655,9 +610,9 @@ fn resize_hover(
area_layer_id: LayerId,
rect: Rect,
) -> Option<WindowInteraction> {
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
let pointer = ctx.input().pointer.interact_pos()?;
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
if ctx.input().pointer.any_down() && !ctx.input().pointer.any_pressed() {
return None; // already dragging (something)
}
@ -667,7 +622,7 @@ fn resize_hover(
}
}
if ctx.memory(|mem| mem.interaction.drag_interest) {
if ctx.memory().interaction.drag_interest {
// Another widget will become active if we drag here
return None;
}
@ -829,8 +784,8 @@ fn show_title_bar(
collapsible: bool,
) -> TitleBar {
let inner_response = ui.horizontal(|ui| {
let height = ui
.fonts(|fonts| title.font_height(fonts, ui.style()))
let height = title
.font_height(&ui.fonts(), ui.style())
.max(ui.spacing().interact_size.y);
ui.set_min_height(height);
@ -924,11 +879,8 @@ impl TitleBar {
ui.painter().hline(outer_rect.x_range(), y, stroke);
}
// Don't cover the close- and collapse buttons:
let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0));
if ui
.interact(double_click_rect, self.id, Sense::click())
.interact(self.rect, self.id, Sense::click())
.double_clicked()
&& collapsible
{

File diff suppressed because it is too large Load diff

View file

@ -13,10 +13,10 @@ use crate::emath::*;
#[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.
/// Position and size of the area that egui should use.
/// Usually you would set this to
///
/// `Some(Rect::from_pos_size(Default::default(), screen_size_in_points))`.
/// `Some(Rect::from_pos_size(Default::default(), screen_size))`.
///
/// but you could also constrain egui to some smaller portion of your window if you like.
///
@ -133,7 +133,7 @@ impl RawInput {
}
/// A file about to be dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct HoveredFile {
/// Set by the `egui-winit` backend.
@ -144,7 +144,7 @@ pub struct HoveredFile {
}
/// A file dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DroppedFile {
/// Set by the `egui-winit` backend.
@ -187,15 +187,6 @@ pub enum Event {
/// 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,
},
@ -277,10 +268,6 @@ pub enum Event {
/// 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)
@ -310,11 +297,7 @@ 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)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Modifiers {
/// Either of the alt keys are down (option ⌥ on Mac).
@ -338,6 +321,10 @@ pub struct Modifiers {
}
impl Modifiers {
pub fn new() -> Self {
Default::default()
}
pub const NONE: Self = Self {
alt: false,
ctrl: false,
@ -367,8 +354,6 @@ impl Modifiers {
mac_cmd: false,
command: false,
};
#[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"]
pub const ALT_SHIFT: Self = Self {
alt: true,
ctrl: false,
@ -395,50 +380,24 @@ impl Modifiers {
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]
#[inline(always)]
pub fn is_none(&self) -> bool {
self == &Self::default()
}
#[inline]
#[inline(always)]
pub fn any(&self) -> bool {
!self.is_none()
}
/// Is shift the only pressed button?
#[inline]
#[inline(always)]
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]
#[inline(always)]
pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command
}
@ -494,82 +453,17 @@ impl Modifiers {
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);
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,
}
s
}
}
// ----------------------------------------------------------------------------
/// Keyboard keys.
///
/// Includes all keys egui is interested in (such as `Home` and `End`)
@ -598,11 +492,6 @@ pub enum Key {
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.
@ -674,151 +563,6 @@ pub enum Key {
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 {

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