Compare commits
85 commits
feat-set-m
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7215fdfb7c | ||
![]() |
e2778d9d6a | ||
![]() |
38849fe381 | ||
![]() |
df7e5bd57a | ||
![]() |
e3e781ced8 | ||
![]() |
97756bc246 | ||
![]() |
f71d79a0ff | ||
![]() |
95247daa17 | ||
![]() |
530e9f667c | ||
![]() |
409fb968d3 | ||
![]() |
1581f0229e | ||
![]() |
ae722ab0cf | ||
![]() |
1384fa3287 | ||
![]() |
83b5b81227 | ||
![]() |
63fa3aec10 | ||
![]() |
ebeb788b1f | ||
![]() |
0fc25c2680 | ||
![]() |
449dd1c23c | ||
![]() |
636a39cbe1 | ||
![]() |
be9b5a3641 | ||
![]() |
e8b9e706ca | ||
![]() |
a8d5a82a7f | ||
![]() |
c2d37571f7 | ||
![]() |
90cd178117 | ||
![]() |
7397be3401 | ||
![]() |
1edd333864 | ||
![]() |
b40dba18c6 | ||
![]() |
4721a0a680 | ||
![]() |
8d6c2580f4 | ||
![]() |
8bc88c9bf4 | ||
![]() |
b52cd2052f | ||
![]() |
b1e214bbdf | ||
![]() |
cef0c0b6d8 | ||
![]() |
d5dcc87ace | ||
![]() |
853d492724 | ||
![]() |
d15ce22e2e | ||
![]() |
628c84cbee | ||
![]() |
212656f3fc | ||
![]() |
660566c499 | ||
![]() |
430cbe541c | ||
![]() |
53f8e4049f | ||
![]() |
f0718a61d3 | ||
![]() |
fb5cb3052d | ||
![]() |
9c270448a6 | ||
![]() |
d15f4ef992 | ||
![]() |
21a59143a4 | ||
![]() |
06d753c289 | ||
![]() |
8344e88f8a | ||
![]() |
e4eaf99072 | ||
![]() |
1353a5733f | ||
![]() |
8aa07e9d43 | ||
![]() |
a6b60d5d58 | ||
![]() |
8c59888ebd | ||
![]() |
5725868b57 | ||
![]() |
8c3d8b3ba5 | ||
![]() |
312cab5355 | ||
![]() |
5444ab269a | ||
![]() |
d01e4342f0 | ||
![]() |
cc20dcb9d0 | ||
![]() |
f222ee044e | ||
![]() |
c72bdb77b5 | ||
![]() |
e7c0547e23 | ||
![]() |
fe7ff66266 | ||
![]() |
ce62b61e15 | ||
![]() |
4a0bafbeab | ||
![]() |
5b1cad2b72 | ||
![]() |
8ce0e1c520 | ||
![]() |
eee4cf6a82 | ||
![]() |
4bd4eca2e4 | ||
![]() |
c75e72693c | ||
![]() |
518b4f447e | ||
![]() |
d4f9f6984d | ||
![]() |
356ebe55da | ||
![]() |
5029575ed0 | ||
![]() |
30e49f1da2 | ||
![]() |
01bbda4544 | ||
![]() |
f87c6cbd7c | ||
![]() |
ce5472633d | ||
![]() |
0ad8aea811 | ||
![]() |
53b1d0e5e9 | ||
![]() |
0eabd894bd | ||
![]() |
cd0f66b9ae | ||
![]() |
60b4f5e3fe | ||
![]() |
f7ed135192 | ||
![]() |
e70204d726 |
181 changed files with 4729 additions and 4011 deletions
6
.cargo/config.toml
Normal file
6
.cargo/config.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# 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"]
|
72
.github/workflows/rust.yml
vendored
72
.github/workflows/rust.yml
vendored
|
@ -16,66 +16,93 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: default
|
profile: default
|
||||||
toolchain: 1.65.0
|
toolchain: 1.65.0
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Install packages (Linux)
|
- name: Install packages (Linux)
|
||||||
if: runner.os == '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: 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
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Rustfmt
|
- name: Rustfmt
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
- name: Install cargo-cranky
|
- name: Install cargo-cranky
|
||||||
uses: baptiste0928/cargo-install@v1
|
uses: baptiste0928/cargo-install@v1
|
||||||
with:
|
with:
|
||||||
crate: cargo-cranky
|
crate: cargo-cranky
|
||||||
- name: Check all features
|
|
||||||
|
- name: check --all-features
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --locked --all-features
|
args: --locked --all-features --all-targets
|
||||||
- name: Check default features
|
|
||||||
|
- name: check default features
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --locked
|
args: --locked --all-targets
|
||||||
- name: Check no default features
|
|
||||||
|
- name: check --no-default-features
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
args: --locked --no-default-features --lib
|
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
|
- name: Test doc-tests
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --doc --all-features
|
args: --doc --all-features
|
||||||
|
|
||||||
- name: cargo doc --lib
|
- name: cargo doc --lib
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: doc
|
command: doc
|
||||||
args: --lib --no-deps --all-features
|
args: --lib --no-deps --all-features
|
||||||
|
|
||||||
- name: cargo doc --document-private-items
|
- name: cargo doc --document-private-items
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: doc
|
command: doc
|
||||||
args: --document-private-items --no-deps --all-features
|
args: --document-private-items --no-deps --all-features
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --all-features
|
args: --all-features
|
||||||
|
|
||||||
- name: Cranky
|
- name: Cranky
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: cranky
|
command: cranky
|
||||||
args: --all-targets --all-features -- -D warnings
|
args: --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
check_wasm:
|
check_wasm:
|
||||||
name: Check wasm32 + wasm-bindgen
|
name: Check wasm32 + wasm-bindgen
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
@ -119,7 +146,7 @@ jobs:
|
||||||
- name: wasm-bindgen
|
- name: wasm-bindgen
|
||||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||||
with:
|
with:
|
||||||
version: "0.2.83"
|
version: "0.2.84"
|
||||||
|
|
||||||
- run: ./sh/wasm_bindgen_check.sh --skip-setup
|
- run: ./sh/wasm_bindgen_check.sh --skip-setup
|
||||||
|
|
||||||
|
@ -129,6 +156,8 @@ jobs:
|
||||||
command: cranky
|
command: cranky
|
||||||
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
|
args: --target wasm32-unknown-unknown --all-features -p egui_demo_app --lib -- -D warnings
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
cargo-deny:
|
cargo-deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
@ -138,18 +167,45 @@ jobs:
|
||||||
with:
|
with:
|
||||||
rust-version: "1.65.0"
|
rust-version: "1.65.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
android:
|
android:
|
||||||
name: android
|
name: android
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: 1.65.0
|
toolchain: 1.65.0
|
||||||
target: aarch64-linux-android
|
target: aarch64-linux-android
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Set up cargo cache
|
- name: Set up cargo cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- run: cargo check --features wgpu --target aarch64-linux-android
|
- run: cargo check --features wgpu --target aarch64-linux-android
|
||||||
working-directory: crates/eframe
|
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
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
|
.DS_Store
|
||||||
**/target
|
**/target
|
||||||
**/target_ra
|
**/target_ra
|
||||||
|
**/target_wasm
|
||||||
/.*.json
|
/.*.json
|
||||||
/.vscode
|
/.vscode
|
||||||
/media/*
|
/media/*
|
||||||
.DS_Store
|
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -5,21 +5,48 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
|
||||||
|
## 0.21.0 - 2023-02-08 - Deadlock fix and style customizability
|
||||||
|
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
|
||||||
|
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
|
||||||
|
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
|
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
|
||||||
|
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
|
||||||
* `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)).
|
* `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 `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 `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 `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 `ScrollArea::drag_to_scroll` if you want to turn off that feature.
|
||||||
* Add `Response::on_hover_and_drag_cursor`.
|
* Add `Response::on_hover_and_drag_cursor`.
|
||||||
|
* Add `Window::default_open` ([#2539](https://github.com/emilk/egui/pull/2539)).
|
||||||
|
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
|
||||||
|
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
|
||||||
|
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
|
||||||
|
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
|
||||||
|
* You can turn off the vertical line left of indented regions with `Visuals::indent_has_left_vline` ([#2636](https://github.com/emilk/egui/pull/2636)).
|
||||||
|
* Add `Response.highlight` to highlight a widget ([#2632](https://github.com/emilk/egui/pull/2632)).
|
||||||
|
* Add `Separator::grow` and `Separator::shrink` ([#2665](https://github.com/emilk/egui/pull/2665)).
|
||||||
|
* Add `Slider::trailing_fill` for trailing color behind the circle like a `ProgressBar` ([#2660](https://github.com/emilk/egui/pull/2660)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).
|
* Improved 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`.
|
* 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 🐛
|
### 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)).
|
* 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)).
|
* 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
|
## 0.20.1 - 2022-12-11 - Fix key-repeat
|
||||||
|
|
1857
Cargo.lock
generated
1857
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,6 @@ members = [
|
||||||
"crates/egui_demo_app",
|
"crates/egui_demo_app",
|
||||||
"crates/egui_demo_lib",
|
"crates/egui_demo_lib",
|
||||||
"crates/egui_extras",
|
"crates/egui_extras",
|
||||||
"crates/egui_glium",
|
|
||||||
"crates/egui_glow",
|
"crates/egui_glow",
|
||||||
"crates/egui-wgpu",
|
"crates/egui-wgpu",
|
||||||
"crates/egui-winit",
|
"crates/egui-winit",
|
||||||
|
@ -27,7 +26,8 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'`
|
||||||
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked" # faster debug builds on mac
|
# 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
|
# opt-level = 1 # Make debug builds run faster
|
||||||
|
|
||||||
# Optimize all dependencies even in debug builds (does not affect workspace packages):
|
# Optimize all dependencies even in debug builds (does not affect workspace packages):
|
||||||
|
|
16
README.md
16
README.md
|
@ -66,11 +66,11 @@ 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/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||||
|
|
||||||
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev`
|
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||||
|
|
||||||
On Fedora Rawhide you need to run:
|
On Fedora Rawhide you need to run:
|
||||||
|
|
||||||
`dnf install clang clang-devel clang-tools-extra speech-dispatcher-devel libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel`
|
||||||
|
|
||||||
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
|
**NOTE**: This is just for the demo app - egui itself is completely platform agnostic!
|
||||||
|
|
||||||
|
@ -158,17 +158,17 @@ An integration needs to do the following each frame:
|
||||||
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
|
* **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui
|
||||||
* Run the application code
|
* Run the application code
|
||||||
* **Output**: Handle egui output (cursor changes, paste, texture allocations, …)
|
* **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_glium/src/painter.rs))
|
* **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs))
|
||||||
|
|
||||||
### Official integrations
|
### Official integrations
|
||||||
|
|
||||||
These are the official egui 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_glow` and `egui-winit`.
|
* [`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_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium).
|
|
||||||
* [`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_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-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-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit).
|
||||||
|
* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) (DEPRECATED - looking for new maintainer).
|
||||||
|
|
||||||
### 3rd party integrations
|
### 3rd party integrations
|
||||||
|
|
||||||
|
@ -186,13 +186,14 @@ These are the official egui integrations:
|
||||||
* [`egui-tetra`](https://crates.io/crates/egui-tetra) for [Tetra](https://crates.io/crates/tetra), a 2D game framework.
|
* [`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).
|
* [`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).
|
* [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs).
|
||||||
* [`ggez-egui`](https://github.com/NemuiSen/ggez-egui) for the [ggez](https://ggez.rs/) game framework.
|
* [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework.
|
||||||
* [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust).
|
* [`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).
|
* [`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).
|
* [`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).
|
* [`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).
|
* [`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/).
|
* [`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!
|
Missing an integration for the thing you're working on? Create one, it's easy!
|
||||||
|
|
||||||
|
@ -371,6 +372,8 @@ 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>.
|
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
|
### 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.
|
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.
|
||||||
|
@ -396,6 +399,7 @@ Notable contributions by:
|
||||||
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
|
* [@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).
|
* [@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).
|
* [@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).
|
* 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).
|
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).
|
||||||
|
|
|
@ -3,6 +3,9 @@ All notable changes to the `ecolor` crate will be noted in this file.
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
|
||||||
|
## 0.21.0 - 2023-02-08
|
||||||
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
|
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||||
"Andreas Reich <reichandreas@gmx.de>",
|
"Andreas Reich <reichandreas@gmx.de>",
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
# ecolor - egui color library
|
# ecolor - egui color library
|
||||||
|
|
||||||
|
[](https://crates.io/crates/ecolor)
|
||||||
|
[](https://docs.rs/ecolor)
|
||||||
|
[](https://github.com/rust-secure-code/safety-dance/)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
A simple color storage and conversion library.
|
A simple color storage and conversion library.
|
||||||
|
|
||||||
Made for [`egui`](https://github.com/emilk/egui/).
|
Made for [`egui`](https://github.com/emilk/egui/).
|
||||||
|
|
|
@ -198,4 +198,19 @@ impl Color32 {
|
||||||
// we need a somewhat expensive conversion to linear space and back.
|
// we need a somewhat expensive conversion to linear space and back.
|
||||||
Rgba::from(self).multiply(factor).into()
|
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,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,37 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||||
|
|
||||||
## Unreleased
|
## 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:
|
#### Desktop/Native:
|
||||||
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
|
* `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
|
## 0.20.1 - 2022-12-11
|
||||||
* Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
* 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
|
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "eframe"
|
name = "eframe"
|
||||||
version = "0.20.1"
|
version = "0.21.3"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -14,9 +14,7 @@ keywords = ["egui", "gui", "gamedev"]
|
||||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# Avoid speech-dispatcher dependencies - see https://docs.rs/crate/eframe/0.20.0/builds/695200
|
all-features = true
|
||||||
no-default-features = true
|
|
||||||
features = ["document-features", "glow", "wgpu", "persistence", "wgpu"]
|
|
||||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -38,7 +36,7 @@ dark-light = ["dep:dark-light"]
|
||||||
default_fonts = ["egui/default_fonts"]
|
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).
|
## 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"]
|
glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
|
||||||
|
|
||||||
## Enable saving app state to disk.
|
## Enable saving app state to disk.
|
||||||
persistence = [
|
persistence = [
|
||||||
|
@ -55,8 +53,10 @@ persistence = [
|
||||||
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
|
## `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", "egui-wgpu?/puffin"]
|
||||||
|
|
||||||
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
|
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||||
screen_reader = ["egui-winit/screen_reader", "tts"]
|
##
|
||||||
|
## For other platforms, use the "accesskit" feature instead.
|
||||||
|
web_screen_reader = ["tts"]
|
||||||
|
|
||||||
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
|
## 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.
|
## This is used to generate images for the examples.
|
||||||
|
@ -68,7 +68,7 @@ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
|
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"tracing",
|
"tracing",
|
||||||
] }
|
] }
|
||||||
|
@ -79,44 +79,38 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
egui_glow = { version = "0.20.0", path = "../egui_glow", optional = true, default-features = false }
|
egui_glow = { version = "0.21.0", path = "../egui_glow", optional = true, default-features = false }
|
||||||
glow = { version = "0.11", optional = true }
|
glow = { version = "0.12", optional = true }
|
||||||
ron = { version = "0.8", optional = true, features = ["integer128"] }
|
ron = { version = "0.8", optional = true, features = ["integer128"] }
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# native:
|
# native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
egui-winit = { version = "0.20.0", path = "../egui-winit", default-features = false, features = [
|
egui-winit = { version = "0.21.1", path = "../egui-winit", default-features = false, features = [
|
||||||
"clipboard",
|
"clipboard",
|
||||||
"links",
|
"links",
|
||||||
] }
|
] }
|
||||||
raw-window-handle = { version = "0.5.0" }
|
raw-window-handle = { version = "0.5.0" }
|
||||||
winit = "0.27.2"
|
winit = "0.28.1"
|
||||||
|
|
||||||
# optional native:
|
# optional native:
|
||||||
dark-light = { version = "0.2.1", optional = true }
|
dark-light = { version = "1.0", optional = true }
|
||||||
directories-next = { version = "2", optional = true }
|
directories-next = { version = "2", optional = true }
|
||||||
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true, features = [
|
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true, features = [
|
||||||
"winit",
|
"winit",
|
||||||
] } # if wgpu is used, use it with winit
|
] } # if wgpu is used, use it with winit
|
||||||
pollster = { version = "0.2", optional = true } # needed for wgpu
|
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.
|
# 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.
|
# this can be done at the same time we expose x11/wayland features of winit crate.
|
||||||
glutin = { version = "0.30.0", optional = true, es = [
|
glutin = { version = "0.30", optional = true }
|
||||||
"egl",
|
glutin-winit = { version = "0.3.0", optional = true }
|
||||||
"glx",
|
|
||||||
"x11",
|
|
||||||
"wayland",
|
|
||||||
"wgl",
|
|
||||||
] }
|
|
||||||
|
|
||||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||||
"png",
|
"png",
|
||||||
] }
|
] }
|
||||||
puffin = { version = "0.14", optional = true }
|
puffin = { version = "0.14", optional = true }
|
||||||
wgpu = { version = "0.14", optional = true }
|
wgpu = { version = "0.15.0", optional = true }
|
||||||
|
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# web:
|
# web:
|
||||||
|
@ -124,7 +118,7 @@ wgpu = { version = "0.14", optional = true }
|
||||||
bytemuck = "1.7"
|
bytemuck = "1.7"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "=0.2.84"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
web-sys = { version = "0.3.58", features = [
|
web-sys = { version = "0.3.58", features = [
|
||||||
"BinaryType",
|
"BinaryType",
|
||||||
|
@ -170,6 +164,6 @@ web-sys = { version = "0.3.58", features = [
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# optional web:
|
# optional web:
|
||||||
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
egui-wgpu = { version = "0.21.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||||
tts = { version = "0.24", optional = true }
|
tts = { version = "0.25", optional = true, default-features = false }
|
||||||
wgpu = { version = "0.14", optional = true, features = ["webgl"] }
|
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }
|
||||||
|
|
|
@ -22,7 +22,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
|
||||||
To use on Linux, first run:
|
To use on Linux, first run:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev
|
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
You 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.
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub use crate::native::run::UserEvent;
|
pub use crate::native::run::UserEvent;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub use winit::event_loop::EventLoopBuilder;
|
pub use winit::event_loop::EventLoopBuilder;
|
||||||
|
|
||||||
/// Hook into the building of an event loop before it is run
|
/// Hook into the building of an event loop before it is run
|
||||||
|
@ -20,6 +22,7 @@ pub use winit::event_loop::EventLoopBuilder;
|
||||||
/// You can configure any platform specific details required on top of the default configuration
|
/// You can configure any platform specific details required on top of the default configuration
|
||||||
/// done by `EFrame`.
|
/// done by `EFrame`.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||||
|
|
||||||
/// This is how your app is created.
|
/// This is how your app is created.
|
||||||
|
@ -149,13 +152,21 @@ pub trait App {
|
||||||
egui::Vec2::INFINITY
|
egui::Vec2::INFINITY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
|
/// Background color values 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.
|
/// This is the background of your windows if you don't set a central panel.
|
||||||
fn clear_color(&self, _visuals: &egui::Visuals) -> egui::Rgba {
|
///
|
||||||
|
/// 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] {
|
||||||
// NOTE: a bright gray makes the shadows of the windows look weird.
|
// 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
|
// We use a bit of transparency so that if the user switches on the
|
||||||
// `transparent()` option they get immediate results.
|
// `transparent()` option they get immediate results.
|
||||||
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
|
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).to_normalized_gamma_f32()
|
||||||
|
|
||||||
// _visuals.window_fill() would also be a natural choice
|
// _visuals.window_fill() would also be a natural choice
|
||||||
}
|
}
|
||||||
|
@ -173,7 +184,7 @@ pub trait App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If `true` a warm-up call to [`Self::update`] will be issued where
|
/// If `true` a warm-up call to [`Self::update`] will be issued where
|
||||||
/// `ctx.memory().everything_is_visible()` will be set to `true`.
|
/// `ctx.memory(|mem| mem.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.
|
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
|
||||||
///
|
///
|
||||||
|
@ -309,6 +320,7 @@ pub struct NativeOptions {
|
||||||
pub hardware_acceleration: HardwareAcceleration,
|
pub hardware_acceleration: HardwareAcceleration,
|
||||||
|
|
||||||
/// What rendering backend to use.
|
/// What rendering backend to use.
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub renderer: Renderer,
|
pub renderer: Renderer,
|
||||||
|
|
||||||
/// Only used if the `dark-light` feature is enabled:
|
/// Only used if the `dark-light` feature is enabled:
|
||||||
|
@ -347,6 +359,7 @@ pub struct NativeOptions {
|
||||||
/// event loop before it is run.
|
/// event loop before it is run.
|
||||||
///
|
///
|
||||||
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
|
@ -373,9 +386,13 @@ impl Clone for NativeOptions {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
icon_data: self.icon_data.clone(),
|
icon_data: self.icon_data.clone(),
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
event_loop_builder: None, // Skip any builder callbacks if cloning
|
event_loop_builder: None, // Skip any builder callbacks if cloning
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
wgpu_options: self.wgpu_options.clone(),
|
wgpu_options: self.wgpu_options.clone(),
|
||||||
|
|
||||||
..*self
|
..*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -389,8 +406,10 @@ impl Default for NativeOptions {
|
||||||
maximized: false,
|
maximized: false,
|
||||||
decorated: true,
|
decorated: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fullsize_content: false,
|
fullsize_content: false,
|
||||||
|
|
||||||
drag_and_drop_support: true,
|
drag_and_drop_support: true,
|
||||||
icon_data: None,
|
icon_data: None,
|
||||||
initial_window_pos: None,
|
initial_window_pos: None,
|
||||||
|
@ -405,14 +424,22 @@ impl Default for NativeOptions {
|
||||||
depth_buffer: 0,
|
depth_buffer: 0,
|
||||||
stencil_buffer: 0,
|
stencil_buffer: 0,
|
||||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
renderer: Renderer::default(),
|
renderer: Renderer::default(),
|
||||||
|
|
||||||
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
|
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
|
||||||
default_theme: Theme::Dark,
|
default_theme: Theme::Dark,
|
||||||
run_and_return: true,
|
run_and_return: true,
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
event_loop_builder: None,
|
event_loop_builder: None,
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
shader_version: None,
|
shader_version: None,
|
||||||
|
|
||||||
centered: false,
|
centered: false,
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||||
}
|
}
|
||||||
|
@ -429,6 +456,7 @@ impl NativeOptions {
|
||||||
match dark_light::detect() {
|
match dark_light::detect() {
|
||||||
dark_light::Mode::Dark => Some(Theme::Dark),
|
dark_light::Mode::Dark => Some(Theme::Dark),
|
||||||
dark_light::Mode::Light => Some(Theme::Light),
|
dark_light::Mode::Light => Some(Theme::Light),
|
||||||
|
dark_light::Mode::Default => None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -550,6 +578,7 @@ pub enum WebGlContextOption {
|
||||||
/// What rendering backend to use.
|
/// What rendering backend to use.
|
||||||
///
|
///
|
||||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
/// 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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||||
|
@ -563,6 +592,7 @@ pub enum Renderer {
|
||||||
Wgpu,
|
Wgpu,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
impl Default for Renderer {
|
impl Default for Renderer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
|
@ -578,6 +608,7 @@ impl Default for Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
impl std::fmt::Display for Renderer {
|
impl std::fmt::Display for Renderer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -590,6 +621,7 @@ impl std::fmt::Display for Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
impl std::str::FromStr for Renderer {
|
impl std::str::FromStr for Renderer {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
|
@ -709,6 +741,18 @@ impl Frame {
|
||||||
self.output.close = true;
|
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.
|
/// Tell `eframe` to close the desktop window.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[deprecated = "Renamed `close`"]
|
#[deprecated = "Renamed `close`"]
|
||||||
|
@ -790,6 +834,7 @@ impl Frame {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for integrations only: call once per frame
|
/// for integrations only: call once per frame
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
|
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
|
||||||
std::mem::take(&mut self.output)
|
std::mem::take(&mut self.output)
|
||||||
}
|
}
|
||||||
|
@ -820,6 +865,12 @@ pub struct WindowInfo {
|
||||||
/// Are we in fullscreen mode?
|
/// Are we in fullscreen mode?
|
||||||
pub fullscreen: bool,
|
pub fullscreen: bool,
|
||||||
|
|
||||||
|
/// Are we minimized?
|
||||||
|
pub minimized: bool,
|
||||||
|
|
||||||
|
/// Are we maximized?
|
||||||
|
pub maximized: bool,
|
||||||
|
|
||||||
/// Window inner size in egui points (logical pixels).
|
/// Window inner size in egui points (logical pixels).
|
||||||
pub size: egui::Vec2,
|
pub size: egui::Vec2,
|
||||||
|
|
||||||
|
@ -1002,5 +1053,13 @@ pub(crate) mod backend {
|
||||||
/// Set to some bool to tell the window always on top.
|
/// Set to some bool to tell the window always on top.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub always_on_top: Option<bool>,
|
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>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,6 +137,7 @@ pub async fn start_web(
|
||||||
// When compiling natively
|
// When compiling natively
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
mod native;
|
mod native;
|
||||||
|
|
||||||
/// This is how you start a native (desktop) app.
|
/// This is how you start a native (desktop) app.
|
||||||
|
@ -179,6 +180,7 @@ mod native;
|
||||||
/// This function can fail if we fail to set up a graphics context.
|
/// This function can fail if we fail to set up a graphics context.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
pub fn run_native(
|
pub fn run_native(
|
||||||
app_name: &str,
|
app_name: &str,
|
||||||
native_options: NativeOptions,
|
native_options: NativeOptions,
|
||||||
|
@ -221,36 +223,40 @@ pub enum Error {
|
||||||
Glutin(#[from] glutin::error::Error),
|
Glutin(#[from] glutin::error::Error),
|
||||||
|
|
||||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||||
#[error("Found no glutin configs matching the template: {0:?}")]
|
#[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")]
|
||||||
NoGlutinConfigs(glutin::config::ConfigTemplate),
|
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
|
||||||
|
|
||||||
#[cfg(feature = "wgpu")]
|
#[cfg(feature = "wgpu")]
|
||||||
#[error("WGPU error: {0}")]
|
#[error("WGPU error: {0}")]
|
||||||
Wgpu(#[from] wgpu::RequestDeviceError),
|
Wgpu(#[from] egui_wgpu::WgpuError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Profiling macro for feature "puffin"
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
macro_rules! profile_function {
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
|
mod profiling_scopes {
|
||||||
|
/// Profiling macro for feature "puffin"
|
||||||
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
puffin::profile_function!($($arg)*);
|
puffin::profile_function!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
pub(crate) use profile_function;
|
||||||
pub(crate) use profile_function;
|
|
||||||
|
|
||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
macro_rules! profile_scope {
|
||||||
macro_rules! profile_scope {
|
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
puffin::profile_scope!($($arg)*);
|
puffin::profile_scope!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
pub(crate) use profile_scope;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) use profile_scope;
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
|
pub(crate) use profiling_scopes::*;
|
||||||
|
|
|
@ -12,6 +12,14 @@ use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
||||||
|
|
||||||
use crate::{epi, Theme, WindowInfo};
|
use crate::{epi, Theme, WindowInfo};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WindowState {
|
||||||
|
// We cannot simply call `winit::Window::is_minimized/is_maximized`
|
||||||
|
// because that deadlocks on mac.
|
||||||
|
pub minimized: bool,
|
||||||
|
pub maximized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||||
winit::dpi::LogicalSize {
|
winit::dpi::LogicalSize {
|
||||||
width: points.x as f64,
|
width: points.x as f64,
|
||||||
|
@ -19,7 +27,11 @@ 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) -> WindowInfo {
|
pub fn read_window_info(
|
||||||
|
window: &winit::window::Window,
|
||||||
|
pixels_per_point: f32,
|
||||||
|
window_state: &WindowState,
|
||||||
|
) -> WindowInfo {
|
||||||
let position = window
|
let position = window
|
||||||
.outer_position()
|
.outer_position()
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -38,9 +50,13 @@ pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -
|
||||||
.inner_size()
|
.inner_size()
|
||||||
.to_logical::<f32>(pixels_per_point.into());
|
.to_logical::<f32>(pixels_per_point.into());
|
||||||
|
|
||||||
|
// NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac.
|
||||||
|
|
||||||
WindowInfo {
|
WindowInfo {
|
||||||
position,
|
position,
|
||||||
fullscreen: window.fullscreen().is_some(),
|
fullscreen: window.fullscreen().is_some(),
|
||||||
|
minimized: window_state.minimized,
|
||||||
|
maximized: window_state.maximized,
|
||||||
size: egui::Vec2 {
|
size: egui::Vec2 {
|
||||||
x: size.width,
|
x: size.width,
|
||||||
y: size.height,
|
y: size.height,
|
||||||
|
@ -56,7 +72,6 @@ pub fn window_builder<E>(
|
||||||
window_settings: Option<WindowSettings>,
|
window_settings: Option<WindowSettings>,
|
||||||
) -> winit::window::WindowBuilder {
|
) -> winit::window::WindowBuilder {
|
||||||
let epi::NativeOptions {
|
let epi::NativeOptions {
|
||||||
always_on_top,
|
|
||||||
maximized,
|
maximized,
|
||||||
decorated,
|
decorated,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
|
@ -70,6 +85,7 @@ pub fn window_builder<E>(
|
||||||
max_window_size,
|
max_window_size,
|
||||||
resizable,
|
resizable,
|
||||||
transparent,
|
transparent,
|
||||||
|
centered,
|
||||||
..
|
..
|
||||||
} = native_options;
|
} = native_options;
|
||||||
|
|
||||||
|
@ -77,7 +93,6 @@ pub fn window_builder<E>(
|
||||||
|
|
||||||
let mut window_builder = winit::window::WindowBuilder::new()
|
let mut window_builder = winit::window::WindowBuilder::new()
|
||||||
.with_title(title)
|
.with_title(title)
|
||||||
.with_always_on_top(*always_on_top)
|
|
||||||
.with_decorations(*decorated)
|
.with_decorations(*decorated)
|
||||||
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
||||||
.with_maximized(*maximized)
|
.with_maximized(*maximized)
|
||||||
|
@ -105,13 +120,16 @@ pub fn window_builder<E>(
|
||||||
|
|
||||||
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
||||||
|
|
||||||
if let Some(mut window_settings) = window_settings {
|
let inner_size_points = if let Some(mut window_settings) = window_settings {
|
||||||
// Restore pos/size from previous session
|
// Restore pos/size from previous session
|
||||||
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
|
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
|
||||||
|
#[cfg(windows)]
|
||||||
|
window_settings.clamp_window_to_sane_position(&event_loop);
|
||||||
window_builder = window_settings.initialize_window(window_builder);
|
window_builder = window_settings.initialize_window(window_builder);
|
||||||
|
window_settings.inner_size_points()
|
||||||
} else {
|
} else {
|
||||||
if let Some(pos) = *initial_window_pos {
|
if let Some(pos) = *initial_window_pos {
|
||||||
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
|
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||||
x: pos.x as f64,
|
x: pos.x as f64,
|
||||||
y: pos.y as f64,
|
y: pos.y as f64,
|
||||||
});
|
});
|
||||||
|
@ -122,11 +140,39 @@ pub fn window_builder<E>(
|
||||||
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
||||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
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
|
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 {
|
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
|
||||||
let mut max_size = egui::Vec2::ZERO;
|
let mut max_size = egui::Vec2::ZERO;
|
||||||
|
|
||||||
|
@ -169,6 +215,7 @@ pub fn handle_app_output(
|
||||||
window: &winit::window::Window,
|
window: &winit::window::Window,
|
||||||
current_pixels_per_point: f32,
|
current_pixels_per_point: f32,
|
||||||
app_output: epi::backend::AppOutput,
|
app_output: epi::backend::AppOutput,
|
||||||
|
window_state: &mut WindowState,
|
||||||
) {
|
) {
|
||||||
let epi::backend::AppOutput {
|
let epi::backend::AppOutput {
|
||||||
close: _,
|
close: _,
|
||||||
|
@ -180,6 +227,8 @@ pub fn handle_app_output(
|
||||||
window_pos,
|
window_pos,
|
||||||
visible: _, // handled in post_present
|
visible: _, // handled in post_present
|
||||||
always_on_top,
|
always_on_top,
|
||||||
|
minimized,
|
||||||
|
maximized,
|
||||||
} = app_output;
|
} = app_output;
|
||||||
|
|
||||||
if let Some(decorated) = decorated {
|
if let Some(decorated) = decorated {
|
||||||
|
@ -216,7 +265,22 @@ pub fn handle_app_output(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(always_on_top) = always_on_top {
|
if let Some(always_on_top) = always_on_top {
|
||||||
window.set_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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,6 +307,7 @@ pub struct EpiIntegration {
|
||||||
/// When set, it is time to close the native window.
|
/// When set, it is time to close the native window.
|
||||||
close: bool,
|
close: bool,
|
||||||
can_drag_window: bool,
|
can_drag_window: bool,
|
||||||
|
window_state: WindowState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EpiIntegration {
|
impl EpiIntegration {
|
||||||
|
@ -257,16 +322,22 @@ impl EpiIntegration {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let egui_ctx = egui::Context::default();
|
let egui_ctx = egui::Context::default();
|
||||||
|
|
||||||
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_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 native_pixels_per_point = window.scale_factor() as f32;
|
||||||
|
|
||||||
|
let window_state = WindowState {
|
||||||
|
minimized: window.is_minimized().unwrap_or(false),
|
||||||
|
maximized: window.is_maximized(),
|
||||||
|
};
|
||||||
|
|
||||||
let frame = epi::Frame {
|
let frame = epi::Frame {
|
||||||
info: epi::IntegrationInfo {
|
info: epi::IntegrationInfo {
|
||||||
system_theme,
|
system_theme,
|
||||||
cpu_usage: None,
|
cpu_usage: None,
|
||||||
native_pixels_per_point: Some(native_pixels_per_point),
|
native_pixels_per_point: Some(native_pixels_per_point),
|
||||||
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
|
window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state),
|
||||||
},
|
},
|
||||||
output: epi::backend::AppOutput {
|
output: epi::backend::AppOutput {
|
||||||
visible: Some(true),
|
visible: Some(true),
|
||||||
|
@ -291,6 +362,7 @@ impl EpiIntegration {
|
||||||
pending_full_output: Default::default(),
|
pending_full_output: Default::default(),
|
||||||
close: false,
|
close: false,
|
||||||
can_drag_window: false,
|
can_drag_window: false,
|
||||||
|
window_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,17 +381,18 @@ impl EpiIntegration {
|
||||||
egui_ctx.enable_accesskit();
|
egui_ctx.enable_accesskit();
|
||||||
// Enqueue a repaint so we'll receive a full tree update soon.
|
// Enqueue a repaint so we'll receive a full tree update soon.
|
||||||
egui_ctx.request_repaint();
|
egui_ctx.request_repaint();
|
||||||
egui::accesskit_placeholder_tree_update()
|
egui_ctx.accesskit_placeholder_tree_update()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
|
||||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
self.egui_ctx
|
||||||
|
.memory_mut(|mem| mem.set_everything_is_visible(true));
|
||||||
let full_output = self.update(app, window);
|
let full_output = self.update(app, window);
|
||||||
self.pending_full_output.append(full_output); // Handle it next frame
|
self.pending_full_output.append(full_output); // Handle it next frame
|
||||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
|
||||||
self.egui_ctx.clear_animations();
|
self.egui_ctx.clear_animations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,12 +444,16 @@ impl EpiIntegration {
|
||||||
) -> egui::FullOutput {
|
) -> egui::FullOutput {
|
||||||
let frame_start = std::time::Instant::now();
|
let frame_start = std::time::Instant::now();
|
||||||
|
|
||||||
self.frame.info.window_info = read_window_info(window, self.egui_ctx.pixels_per_point());
|
self.frame.info.window_info =
|
||||||
|
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
|
||||||
let raw_input = self.egui_winit.take_egui_input(window);
|
let raw_input = self.egui_winit.take_egui_input(window);
|
||||||
|
|
||||||
|
// Run user code:
|
||||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||||
crate::profile_scope!("App::update");
|
crate::profile_scope!("App::update");
|
||||||
app.update(egui_ctx, &mut self.frame);
|
app.update(egui_ctx, &mut self.frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.pending_full_output.append(full_output);
|
self.pending_full_output.append(full_output);
|
||||||
let full_output = std::mem::take(&mut self.pending_full_output);
|
let full_output = std::mem::take(&mut self.pending_full_output);
|
||||||
|
|
||||||
|
@ -389,7 +466,12 @@ impl EpiIntegration {
|
||||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||||
}
|
}
|
||||||
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
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);
|
handle_app_output(
|
||||||
|
window,
|
||||||
|
self.egui_ctx.pixels_per_point(),
|
||||||
|
app_output,
|
||||||
|
&mut self.window_state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
|
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
|
||||||
|
@ -446,7 +528,8 @@ impl EpiIntegration {
|
||||||
}
|
}
|
||||||
if _app.persist_egui_memory() {
|
if _app.persist_egui_memory() {
|
||||||
crate::profile_scope!("egui_memory");
|
crate::profile_scope!("egui_memory");
|
||||||
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
|
self.egui_ctx
|
||||||
|
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
crate::profile_scope!("App::save");
|
crate::profile_scope!("App::save");
|
||||||
|
|
|
@ -38,6 +38,7 @@ pub use epi::NativeOptions;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum EventResult {
|
enum EventResult {
|
||||||
Wait,
|
Wait,
|
||||||
|
|
||||||
/// Causes a synchronous repaint inside the event handler. This should only
|
/// Causes a synchronous repaint inside the event handler. This should only
|
||||||
/// be used in special situations if the window must be repainted while
|
/// be used in special situations if the window must be repainted while
|
||||||
/// handling a specific event. This occurs on Windows when handling resizes.
|
/// handling a specific event. This occurs on Windows when handling resizes.
|
||||||
|
@ -45,20 +46,28 @@ enum EventResult {
|
||||||
/// `RepaintNow` creates a new frame synchronously, and should therefore
|
/// `RepaintNow` creates a new frame synchronously, and should therefore
|
||||||
/// only be used for extremely urgent repaints.
|
/// only be used for extremely urgent repaints.
|
||||||
RepaintNow,
|
RepaintNow,
|
||||||
|
|
||||||
/// Queues a repaint for once the event loop handles its next redraw. Exists
|
/// Queues a repaint for once the event loop handles its next redraw. Exists
|
||||||
/// so that multiple input events can be handled in one frame. Does not
|
/// so that multiple input events can be handled in one frame. Does not
|
||||||
/// cause any delay like `RepaintNow`.
|
/// cause any delay like `RepaintNow`.
|
||||||
RepaintNext,
|
RepaintNext,
|
||||||
|
|
||||||
RepaintAt(Instant),
|
RepaintAt(Instant),
|
||||||
|
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait WinitApp {
|
trait WinitApp {
|
||||||
fn is_focused(&self) -> bool;
|
fn is_focused(&self) -> bool;
|
||||||
|
|
||||||
fn integration(&self) -> Option<&EpiIntegration>;
|
fn integration(&self) -> Option<&EpiIntegration>;
|
||||||
|
|
||||||
fn window(&self) -> Option<&winit::window::Window>;
|
fn window(&self) -> Option<&winit::window::Window>;
|
||||||
|
|
||||||
fn save_and_destroy(&mut self);
|
fn save_and_destroy(&mut self);
|
||||||
|
|
||||||
fn paint(&mut self) -> EventResult;
|
fn paint(&mut self) -> EventResult;
|
||||||
|
|
||||||
fn on_event(
|
fn on_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
|
@ -152,8 +161,8 @@ fn run_and_return(
|
||||||
event => match winit_app.on_event(event_loop, event) {
|
event => match winit_app.on_event(event_loop, event) {
|
||||||
Ok(event_result) => event_result,
|
Ok(event_result) => event_result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
tracing::error!("Exiting because of error: {err:?} on event {event:?}");
|
||||||
returned_result = Err(err);
|
returned_result = Err(err);
|
||||||
tracing::debug!("Exiting because of an error");
|
|
||||||
EventResult::Exit
|
EventResult::Exit
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -290,27 +299,6 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn center_window_pos(
|
|
||||||
monitor: Option<winit::monitor::MonitorHandle>,
|
|
||||||
native_options: &mut epi::NativeOptions,
|
|
||||||
) {
|
|
||||||
// Get the current_monitor.
|
|
||||||
if let Some(monitor) = monitor {
|
|
||||||
let monitor_size = monitor.size();
|
|
||||||
let inner_size = native_options
|
|
||||||
.initial_window_size
|
|
||||||
.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;
|
|
||||||
native_options.initial_window_pos = Some(egui::Pos2 {
|
|
||||||
x: x as _,
|
|
||||||
y: y as _,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
/// Run an egui app
|
/// Run an egui app
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
|
@ -318,6 +306,12 @@ mod glow_integration {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use egui::NumExt as _;
|
use egui::NumExt as _;
|
||||||
|
use glutin::{
|
||||||
|
display::GetGlDisplay,
|
||||||
|
prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext},
|
||||||
|
surface::GlSurface,
|
||||||
|
};
|
||||||
|
use raw_window_handle::HasRawWindowHandle;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -342,132 +336,260 @@ mod glow_integration {
|
||||||
painter: egui_glow::Painter,
|
painter: egui_glow::Painter,
|
||||||
integration: epi_integration::EpiIntegration,
|
integration: epi_integration::EpiIntegration,
|
||||||
app: Box<dyn epi::App>,
|
app: Box<dyn epi::App>,
|
||||||
|
|
||||||
// Conceptually this will be split out eventually so that the rest of the state
|
// Conceptually this will be split out eventually so that the rest of the state
|
||||||
// can be persistent.
|
// can be persistent.
|
||||||
gl_window: GlutinWindowContext,
|
gl_window: GlutinWindowContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This struct will contain both persistent and temporary glutin state.
|
||||||
|
///
|
||||||
|
/// Platform Quirks:
|
||||||
|
/// * Microsoft Windows: requires that we create a window before opengl context.
|
||||||
|
/// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event.
|
||||||
|
///
|
||||||
|
/// winit guarantees that we will get a Resumed event on startup on all platforms.
|
||||||
|
/// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`.
|
||||||
|
/// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android.
|
||||||
|
/// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event.
|
||||||
|
///
|
||||||
|
/// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of
|
||||||
|
/// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended.
|
||||||
struct GlutinWindowContext {
|
struct GlutinWindowContext {
|
||||||
window: winit::window::Window,
|
builder: winit::window::WindowBuilder,
|
||||||
gl_context: glutin::context::PossiblyCurrentContext,
|
swap_interval: glutin::surface::SwapInterval,
|
||||||
gl_display: glutin::display::Display,
|
gl_config: glutin::config::Config,
|
||||||
gl_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
|
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
|
||||||
|
gl_surface: Option<glutin::surface::Surface<glutin::surface::WindowSurface>>,
|
||||||
|
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
|
||||||
|
window: Option<winit::window::Window>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlutinWindowContext {
|
impl GlutinWindowContext {
|
||||||
// refactor this function to use `glutin-winit` crate eventually.
|
/// There is a lot of complexity with opengl creation, so prefer extensivve logging to get all the help we can to debug issues.
|
||||||
// preferably add android support at the same time.
|
///
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
unsafe fn new(
|
unsafe fn new(
|
||||||
winit_window: winit::window::Window,
|
winit_window_builder: winit::window::WindowBuilder,
|
||||||
native_options: &epi::NativeOptions,
|
native_options: &epi::NativeOptions,
|
||||||
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
use glutin::prelude::*;
|
use glutin::prelude::*;
|
||||||
use raw_window_handle::*;
|
// convert native options to glutin options
|
||||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||||
crate::HardwareAcceleration::Required => Some(true),
|
crate::HardwareAcceleration::Required => Some(true),
|
||||||
crate::HardwareAcceleration::Preferred => None,
|
crate::HardwareAcceleration::Preferred => None,
|
||||||
crate::HardwareAcceleration::Off => Some(false),
|
crate::HardwareAcceleration::Off => Some(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let raw_display_handle = winit_window.raw_display_handle();
|
|
||||||
let raw_window_handle = winit_window.raw_window_handle();
|
|
||||||
|
|
||||||
// EGL is crossplatform and the official khronos way
|
|
||||||
// but sometimes platforms/drivers may not have it, so we use back up options where possible.
|
|
||||||
// TODO: check whether we can expose these options as "features", so that users can select the relevant backend they want.
|
|
||||||
|
|
||||||
// try egl and fallback to windows wgl. Windows is the only platform that *requires* window handle to create display.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let preference =
|
|
||||||
glutin::display::DisplayApiPreference::EglThenWgl(Some(raw_window_handle));
|
|
||||||
// try egl and fallback to x11 glx
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let preference = glutin::display::DisplayApiPreference::EglThenGlx(Box::new(
|
|
||||||
winit::platform::unix::register_xlib_error_hook,
|
|
||||||
));
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let preference = glutin::display::DisplayApiPreference::Cgl;
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
let preference = glutin::display::DisplayApiPreference::Egl;
|
|
||||||
|
|
||||||
let gl_display = glutin::display::Display::new(raw_display_handle, preference)?;
|
|
||||||
let swap_interval = if native_options.vsync {
|
let swap_interval = if native_options.vsync {
|
||||||
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
|
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
|
||||||
} else {
|
} else {
|
||||||
glutin::surface::SwapInterval::DontWait
|
glutin::surface::SwapInterval::DontWait
|
||||||
};
|
};
|
||||||
|
/* opengl setup flow goes like this:
|
||||||
let config_template = glutin::config::ConfigTemplateBuilder::new()
|
1. we create a configuration for opengl "Display" / "Config" creation
|
||||||
|
2. choose between special extensions like glx or egl or wgl and use them to create config/display
|
||||||
|
3. opengl context configuration
|
||||||
|
4. opengl context creation
|
||||||
|
*/
|
||||||
|
// start building config for gl display
|
||||||
|
let config_template_builder = glutin::config::ConfigTemplateBuilder::new()
|
||||||
.prefer_hardware_accelerated(hardware_acceleration)
|
.prefer_hardware_accelerated(hardware_acceleration)
|
||||||
.with_depth_size(native_options.depth_buffer);
|
.with_depth_size(native_options.depth_buffer)
|
||||||
|
.with_stencil_size(native_options.stencil_buffer)
|
||||||
|
.with_transparency(native_options.transparent);
|
||||||
// we don't know if multi sampling option is set. so, check if its more than 0.
|
// we don't know if multi sampling option is set. so, check if its more than 0.
|
||||||
let config_template = if native_options.multisampling > 0 {
|
let config_template_builder = if native_options.multisampling > 0 {
|
||||||
config_template.with_multisampling(
|
config_template_builder.with_multisampling(
|
||||||
native_options
|
native_options
|
||||||
.multisampling
|
.multisampling
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("failed to fit multisamples into u8"),
|
.expect("failed to fit multisamples option of native_options into u8"),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
config_template
|
config_template_builder
|
||||||
};
|
};
|
||||||
let config_template = config_template
|
|
||||||
.with_stencil_size(native_options.stencil_buffer)
|
|
||||||
.with_transparency(native_options.transparent)
|
|
||||||
.compatible_with_native_window(raw_window_handle)
|
|
||||||
.build();
|
|
||||||
// finds all valid configurations supported by this display that match the config_template
|
|
||||||
// this is where we will try to get a "fallback" config if we are okay with ignoring some native
|
|
||||||
// options required by user like multi sampling, srgb, transparency etc..
|
|
||||||
// TODO: need to figure out a good fallback config template
|
|
||||||
let config = gl_display
|
|
||||||
.find_configs(config_template.clone())?
|
|
||||||
.next()
|
|
||||||
.ok_or(crate::Error::NoGlutinConfigs(config_template))?;
|
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"trying to create glutin Display with config: {:?}",
|
||||||
|
&config_template_builder
|
||||||
|
);
|
||||||
|
// create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android.
|
||||||
|
let (window, gl_config) = glutin_winit::DisplayBuilder::new()
|
||||||
|
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
|
||||||
|
.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.clone(),
|
||||||
|
|mut config_iterator| {
|
||||||
|
let config = config_iterator.next().expect(
|
||||||
|
"failed to find a matching configuration for creating glutin config",
|
||||||
|
);
|
||||||
|
tracing::debug!(
|
||||||
|
"using the first config from config picker closure. config: {:?}",
|
||||||
|
&config
|
||||||
|
);
|
||||||
|
config
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
|
||||||
|
|
||||||
|
let gl_display = gl_config.display();
|
||||||
|
tracing::debug!(
|
||||||
|
"successfully created GL Display with version: {} and supported features: {:?}",
|
||||||
|
gl_display.version_string(),
|
||||||
|
gl_display.supported_features()
|
||||||
|
);
|
||||||
|
let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle());
|
||||||
|
tracing::debug!(
|
||||||
|
"creating gl context using raw window handle: {:?}",
|
||||||
|
raw_window_handle
|
||||||
|
);
|
||||||
|
|
||||||
|
// create gl context. if core context cannot be created, try gl es context as fallback.
|
||||||
let context_attributes =
|
let context_attributes =
|
||||||
glutin::context::ContextAttributesBuilder::new().build(Some(raw_window_handle));
|
glutin::context::ContextAttributesBuilder::new().build(raw_window_handle);
|
||||||
// for surface creation.
|
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
|
||||||
let (width, height): (u32, u32) = winit_window.inner_size().into();
|
.with_context_api(glutin::context::ContextApi::Gles(None))
|
||||||
|
.build(raw_window_handle);
|
||||||
|
let gl_context = match gl_config
|
||||||
|
.display()
|
||||||
|
.create_context(&gl_config, &context_attributes)
|
||||||
|
{
|
||||||
|
Ok(it) => it,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}");
|
||||||
|
tracing::debug!("retrying with fallback context attributes: {fallback_context_attributes:?}");
|
||||||
|
gl_config
|
||||||
|
.display()
|
||||||
|
.create_context(&gl_config, &fallback_context_attributes)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let not_current_gl_context = Some(gl_context);
|
||||||
|
|
||||||
|
// the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but
|
||||||
|
// it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might
|
||||||
|
// help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc..
|
||||||
|
// https://github.com/emilk/egui/pull/2541#issuecomment-1370767582
|
||||||
|
Ok(GlutinWindowContext {
|
||||||
|
builder: winit_window_builder,
|
||||||
|
swap_interval,
|
||||||
|
gl_config,
|
||||||
|
current_gl_context: None,
|
||||||
|
window,
|
||||||
|
gl_surface: None,
|
||||||
|
not_current_gl_context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime.
|
||||||
|
/// roughly,
|
||||||
|
/// 1. check if window already exists. otherwise, create one now.
|
||||||
|
/// 2. create attributes for surface creation.
|
||||||
|
/// 3. create surface.
|
||||||
|
/// 4. make surface and context current.
|
||||||
|
///
|
||||||
|
/// we presently assume that we will
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
||||||
|
if self.gl_surface.is_some() {
|
||||||
|
tracing::warn!(
|
||||||
|
"on_resume called even thought we already have a surface. early return"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
tracing::debug!("running on_resume fn.");
|
||||||
|
// make sure we have a window or create one.
|
||||||
|
let window = self.window.take().unwrap_or_else(|| {
|
||||||
|
tracing::debug!("window doesn't exist yet. creating one now with finalize_window");
|
||||||
|
glutin_winit::finalize_window(event_loop, self.builder.clone(), &self.gl_config)
|
||||||
|
.expect("failed to finalize glutin window")
|
||||||
|
});
|
||||||
|
// surface attributes
|
||||||
|
let (width, height): (u32, u32) = window.inner_size().into();
|
||||||
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
|
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
|
||||||
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
|
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
|
||||||
let surface_attributes =
|
let surface_attributes =
|
||||||
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
|
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
|
||||||
.build(raw_window_handle, width, height);
|
.build(window.raw_window_handle(), width, height);
|
||||||
// start creating the gl objects
|
tracing::debug!(
|
||||||
let gl_context = gl_display.create_context(&config, &context_attributes)?;
|
"creating surface with attributes: {:?}",
|
||||||
|
&surface_attributes
|
||||||
|
);
|
||||||
|
// create surface
|
||||||
|
let gl_surface = unsafe {
|
||||||
|
self.gl_config
|
||||||
|
.display()
|
||||||
|
.create_window_surface(&self.gl_config, &surface_attributes)?
|
||||||
|
};
|
||||||
|
tracing::debug!("surface created successfully: {gl_surface:?}.making context current");
|
||||||
|
// make surface and context current.
|
||||||
|
let not_current_gl_context = self
|
||||||
|
.not_current_gl_context
|
||||||
|
.take()
|
||||||
|
.expect("failed to get not current context after resume event. impossible!");
|
||||||
|
let current_gl_context = not_current_gl_context.make_current(&gl_surface)?;
|
||||||
|
// try setting swap interval. but its not absolutely necessary, so don't panic on failure.
|
||||||
|
tracing::debug!("made context current. setting swap interval for surface");
|
||||||
|
if let Err(e) = gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) {
|
||||||
|
tracing::error!("failed to set swap interval due to error: {e:?}");
|
||||||
|
}
|
||||||
|
// we will reach this point only once in most platforms except android.
|
||||||
|
// create window/surface/make context current once and just use them forever.
|
||||||
|
self.gl_surface = Some(gl_surface);
|
||||||
|
self.current_gl_context = Some(current_gl_context);
|
||||||
|
self.window = Some(window);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
let gl_surface = gl_display.create_window_surface(&config, &surface_attributes)?;
|
/// only applies for android. but we basically drop surface + window and make context not current
|
||||||
let gl_context = gl_context.make_current(&gl_surface)?;
|
fn on_suspend(&mut self) -> Result<()> {
|
||||||
gl_surface.set_swap_interval(&gl_context, swap_interval)?;
|
tracing::debug!("received suspend event. dropping window and surface");
|
||||||
Ok(GlutinWindowContext {
|
self.gl_surface.take();
|
||||||
window: winit_window,
|
self.window.take();
|
||||||
gl_context,
|
if let Some(current) = self.current_gl_context.take() {
|
||||||
gl_display,
|
tracing::debug!("context is current, so making it non-current");
|
||||||
gl_surface,
|
self.not_current_gl_context = Some(current.make_not_current()?);
|
||||||
})
|
} else {
|
||||||
|
tracing::debug!(
|
||||||
|
"context is already not current??? could be duplicate suspend event"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window(&self) -> &winit::window::Window {
|
fn window(&self) -> &winit::window::Window {
|
||||||
&self.window
|
self.window.as_ref().expect("winit window doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
|
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
|
||||||
use glutin::surface::GlSurface;
|
|
||||||
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
|
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
|
||||||
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
|
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
|
||||||
self.gl_surface.resize(&self.gl_context, width, height);
|
self.gl_surface
|
||||||
|
.as_ref()
|
||||||
|
.expect("failed to get surface to resize")
|
||||||
|
.resize(
|
||||||
|
self.current_gl_context
|
||||||
|
.as_ref()
|
||||||
|
.expect("failed to get current context to resize surface"),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn swap_buffers(&self) -> glutin::error::Result<()> {
|
fn swap_buffers(&self) -> glutin::error::Result<()> {
|
||||||
use glutin::surface::GlSurface;
|
self.gl_surface
|
||||||
self.gl_surface.swap_buffers(&self.gl_context)
|
.as_ref()
|
||||||
|
.expect("failed to get surface to swap buffers")
|
||||||
|
.swap_buffers(
|
||||||
|
self.current_gl_context
|
||||||
|
.as_ref()
|
||||||
|
.expect("failed to get current context to swap buffers"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
|
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
|
||||||
use glutin::display::GlDisplay;
|
self.gl_config.display().get_proc_address(addr)
|
||||||
self.gl_display.get_proc_address(addr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,12 +637,17 @@ mod glow_integration {
|
||||||
|
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
|
|
||||||
let winit_window =
|
let winit_window_builder =
|
||||||
epi_integration::window_builder(event_loop, title, native_options, window_settings)
|
epi_integration::window_builder(event_loop, title, native_options, window_settings);
|
||||||
.build(event_loop)?;
|
let mut glutin_window_context = unsafe {
|
||||||
// a lot of the code below has been lifted from glutin example in their repo.
|
GlutinWindowContext::new(winit_window_builder, native_options, event_loop)?
|
||||||
let glutin_window_context =
|
};
|
||||||
unsafe { GlutinWindowContext::new(winit_window, native_options)? };
|
glutin_window_context.on_resume(event_loop)?;
|
||||||
|
|
||||||
|
if let Some(window) = &glutin_window_context.window {
|
||||||
|
epi_integration::apply_native_options_to_window(window, native_options);
|
||||||
|
}
|
||||||
|
|
||||||
let gl = unsafe {
|
let gl = unsafe {
|
||||||
glow::Context::from_loader_function(|s| {
|
glow::Context::from_loader_function(|s| {
|
||||||
let s = std::ffi::CString::new(s)
|
let s = std::ffi::CString::new(s)
|
||||||
|
@ -749,26 +876,24 @@ mod glow_integration {
|
||||||
) -> Result<EventResult> {
|
) -> Result<EventResult> {
|
||||||
Ok(match event {
|
Ok(match event {
|
||||||
winit::event::Event::Resumed => {
|
winit::event::Event::Resumed => {
|
||||||
|
// first resume event.
|
||||||
|
// we can actually move this outside of event loop.
|
||||||
|
// and just run the on_resume fn of gl_window
|
||||||
if self.running.is_none() {
|
if self.running.is_none() {
|
||||||
self.init_run_state(event_loop)?;
|
self.init_run_state(event_loop)?;
|
||||||
|
} else {
|
||||||
|
// not the first resume event. create whatever you need.
|
||||||
|
self.running
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.gl_window
|
||||||
|
.on_resume(event_loop)?;
|
||||||
}
|
}
|
||||||
EventResult::RepaintNow
|
EventResult::RepaintNow
|
||||||
}
|
}
|
||||||
winit::event::Event::Suspended => {
|
winit::event::Event::Suspended => {
|
||||||
#[cfg(target_os = "android")]
|
self.running.as_mut().unwrap().gl_window.on_suspend()?;
|
||||||
{
|
|
||||||
tracing::error!("Suspended app can't destroy Window surface state with current Egui Glow backend (undefined behaviour)");
|
|
||||||
// Instead of destroying everything which we _know_ we can't re-create
|
|
||||||
// we instead currently just try our luck with not destroying anything.
|
|
||||||
//
|
|
||||||
// When the application resumes then it will get a new `SurfaceView` but
|
|
||||||
// we have no practical way currently of creating a new EGL surface
|
|
||||||
// via the Glutin API while keeping the GL context and the rest of
|
|
||||||
// our app state. This will likely result in a black screen or
|
|
||||||
// frozen screen.
|
|
||||||
//
|
|
||||||
//self.running = None;
|
|
||||||
}
|
|
||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -863,22 +988,13 @@ mod glow_integration {
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if native_options.run_and_return {
|
if native_options.run_and_return {
|
||||||
with_event_loop(native_options, |event_loop, mut native_options| {
|
with_event_loop(native_options, |event_loop, native_options| {
|
||||||
if native_options.centered {
|
|
||||||
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let glow_eframe =
|
let glow_eframe =
|
||||||
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||||
run_and_return(event_loop, glow_eframe)
|
run_and_return(event_loop, glow_eframe)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let event_loop = create_event_loop_builder(&mut native_options).build();
|
let event_loop = create_event_loop_builder(&mut native_options).build();
|
||||||
|
|
||||||
if native_options.centered {
|
|
||||||
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, glow_eframe);
|
run_and_exit(event_loop, glow_eframe);
|
||||||
}
|
}
|
||||||
|
@ -946,19 +1062,20 @@ mod wgpu_integration {
|
||||||
storage: Option<&dyn epi::Storage>,
|
storage: Option<&dyn epi::Storage>,
|
||||||
title: &str,
|
title: &str,
|
||||||
native_options: &NativeOptions,
|
native_options: &NativeOptions,
|
||||||
) -> Result<winit::window::Window> {
|
) -> std::result::Result<winit::window::Window, winit::error::OsError> {
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
Ok(
|
let window_builder =
|
||||||
epi_integration::window_builder(event_loop, title, native_options, window_settings)
|
epi_integration::window_builder(event_loop, title, native_options, window_settings);
|
||||||
.build(event_loop)?,
|
let window = window_builder.build(event_loop)?;
|
||||||
)
|
epi_integration::apply_native_options_to_window(&window, native_options);
|
||||||
|
Ok(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
fn set_window(
|
fn set_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: winit::window::Window,
|
window: winit::window::Window,
|
||||||
) -> std::result::Result<(), wgpu::RequestDeviceError> {
|
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||||
self.window = Some(window);
|
self.window = Some(window);
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -970,7 +1087,7 @@ mod wgpu_integration {
|
||||||
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
fn drop_window(&mut self) -> std::result::Result<(), wgpu::RequestDeviceError> {
|
fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||||
self.window = None;
|
self.window = None;
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -985,13 +1102,14 @@ mod wgpu_integration {
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
storage: Option<Box<dyn epi::Storage>>,
|
storage: Option<Box<dyn epi::Storage>>,
|
||||||
window: winit::window::Window,
|
window: winit::window::Window,
|
||||||
) -> std::result::Result<(), wgpu::RequestDeviceError> {
|
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||||
let painter = unsafe {
|
let painter = unsafe {
|
||||||
let mut painter = egui_wgpu::winit::Painter::new(
|
let mut painter = egui_wgpu::winit::Painter::new(
|
||||||
self.native_options.wgpu_options.clone(),
|
self.native_options.wgpu_options.clone(),
|
||||||
self.native_options.multisampling.max(1) as _,
|
self.native_options.multisampling.max(1) as _,
|
||||||
self.native_options.depth_buffer,
|
self.native_options.depth_buffer,
|
||||||
|
self.native_options.transparent,
|
||||||
);
|
);
|
||||||
pollster::block_on(painter.set_window(Some(&window)))?;
|
pollster::block_on(painter.set_window(Some(&window)))?;
|
||||||
painter
|
painter
|
||||||
|
@ -1286,22 +1404,13 @@ mod wgpu_integration {
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if native_options.run_and_return {
|
if native_options.run_and_return {
|
||||||
with_event_loop(native_options, |event_loop, mut native_options| {
|
with_event_loop(native_options, |event_loop, native_options| {
|
||||||
if native_options.centered {
|
|
||||||
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let wgpu_eframe =
|
let wgpu_eframe =
|
||||||
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||||
run_and_return(event_loop, wgpu_eframe)
|
run_and_return(event_loop, wgpu_eframe)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let event_loop = create_event_loop_builder(&mut native_options).build();
|
let event_loop = create_event_loop_builder(&mut native_options).build();
|
||||||
|
|
||||||
if native_options.centered {
|
|
||||||
center_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, wgpu_eframe);
|
run_and_exit(event_loop, wgpu_eframe);
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,10 +313,11 @@ impl AppRunner {
|
||||||
|
|
||||||
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
||||||
if self.app.warm_up_enabled() {
|
if self.app.warm_up_enabled() {
|
||||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
|
||||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
self.egui_ctx
|
||||||
|
.memory_mut(|m| m.set_everything_is_visible(true));
|
||||||
self.logic()?;
|
self.logic()?;
|
||||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
|
||||||
self.egui_ctx.clear_animations();
|
self.egui_ctx.clear_animations();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -388,7 +389,7 @@ impl AppRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
||||||
if self.egui_ctx.options().screen_reader {
|
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||||
self.screen_reader
|
self.screen_reader
|
||||||
.speak(&platform_output.events_description());
|
.speak(&platform_output.events_description());
|
||||||
}
|
}
|
||||||
|
@ -449,8 +450,6 @@ pub enum EventToUnsubscribe {
|
||||||
|
|
||||||
impl EventToUnsubscribe {
|
impl EventToUnsubscribe {
|
||||||
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
EventToUnsubscribe::TargetEvent(handle) => {
|
EventToUnsubscribe::TargetEvent(handle) => {
|
||||||
handle.target.remove_event_listener_with_callback(
|
handle.target.remove_event_listener_with_callback(
|
||||||
|
@ -467,6 +466,7 @@ impl EventToUnsubscribe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppRunnerContainer {
|
pub struct AppRunnerContainer {
|
||||||
pub runner: AppRunnerRef,
|
pub runner: AppRunnerRef,
|
||||||
|
|
||||||
|
@ -485,8 +485,6 @@ impl AppRunnerContainer {
|
||||||
event_name: &'static str,
|
event_name: &'static str,
|
||||||
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
// Create a JS closure based on the FnMut provided
|
// Create a JS closure based on the FnMut provided
|
||||||
let closure = Closure::wrap({
|
let closure = Closure::wrap({
|
||||||
// Clone atomics
|
// Clone atomics
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use super::*;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
use egui::Key;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
struct IsDestroyed(pub bool);
|
struct IsDestroyed(pub bool);
|
||||||
|
|
||||||
pub fn paint_and_schedule(
|
pub fn paint_and_schedule(
|
||||||
|
@ -28,7 +31,6 @@ pub fn paint_and_schedule(
|
||||||
runner_ref: AppRunnerRef,
|
runner_ref: AppRunnerRef,
|
||||||
panicked: Arc<AtomicBool>,
|
panicked: Arc<AtomicBool>,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
|
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
|
||||||
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||||
|
@ -64,8 +66,9 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
runner_lock.input.raw.modifiers = modifiers;
|
runner_lock.input.raw.modifiers = modifiers;
|
||||||
|
|
||||||
let key = event.key();
|
let key = event.key();
|
||||||
|
let egui_key = translate_key(&key);
|
||||||
|
|
||||||
if let Some(key) = translate_key(&key) {
|
if let Some(key) = egui_key {
|
||||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
|
@ -85,10 +88,18 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
|
|
||||||
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
||||||
|
|
||||||
let prevent_default = if matches!(event.key().as_str(), "Tab") {
|
#[allow(clippy::if_same_then_else)]
|
||||||
|
let prevent_default = if egui_key == Some(Key::Tab) {
|
||||||
// Always prevent moving cursor to url bar.
|
// Always prevent moving cursor to url bar.
|
||||||
// egui wants to use tab to move to the next text field.
|
// egui wants to use tab to move to the next text field.
|
||||||
true
|
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 {
|
} else if egui_wants_keyboard {
|
||||||
matches!(
|
matches!(
|
||||||
event.key().as_str(),
|
event.key().as_str(),
|
||||||
|
@ -112,6 +123,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
|
|
||||||
if prevent_default {
|
if prevent_default {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
// event.stop_propagation();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
@ -198,15 +210,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||||
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||||
|
|
||||||
{
|
let prevent_default_events = [
|
||||||
// By default, right-clicks open a context menu.
|
// By default, right-clicks open a context menu.
|
||||||
// We don't want to do that (right clicks is handled by egui):
|
// We don't want to do that (right clicks is handled by egui):
|
||||||
let event_name = "contextmenu";
|
"contextmenu",
|
||||||
|
// Allow users to use ctrl-p for e.g. a command palette
|
||||||
|
"afterprint",
|
||||||
|
];
|
||||||
|
|
||||||
|
for event_name in prevent_default_events {
|
||||||
let closure =
|
let closure =
|
||||||
move |event: web_sys::MouseEvent,
|
move |event: web_sys::MouseEvent,
|
||||||
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
// event.stop_propagation();
|
||||||
|
// tracing::debug!("Preventing event {:?}", event_name);
|
||||||
};
|
};
|
||||||
|
|
||||||
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||||
|
@ -390,7 +408,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||||
}
|
}
|
||||||
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
||||||
#[allow(clippy::let_and_return)]
|
#[allow(clippy::let_and_return)]
|
||||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
|
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||||
points_per_scroll_line
|
points_per_scroll_line
|
||||||
}
|
}
|
||||||
_ => 1.0, // DOM_DELTA_PIXEL
|
_ => 1.0, // DOM_DELTA_PIXEL
|
||||||
|
|
|
@ -83,7 +83,6 @@ pub fn system_theme() -> Option<Theme> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let document = web_sys::window()?.document()?;
|
let document = web_sys::window()?.document()?;
|
||||||
let canvas = document.get_element_by_id(canvas_id)?;
|
let canvas = document.get_element_by_id(canvas_id)?;
|
||||||
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
|
||||||
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
||||||
match ron::from_str(&memory_string) {
|
match ron::from_str(&memory_string) {
|
||||||
Ok(memory) => {
|
Ok(memory) => {
|
||||||
*ctx.memory() = memory;
|
ctx.memory_mut(|m| *m = memory);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("Failed to parse memory RON: {}", err);
|
tracing::error!("Failed to parse memory RON: {}", err);
|
||||||
|
@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
|
||||||
|
|
||||||
#[cfg(feature = "persistence")]
|
#[cfg(feature = "persistence")]
|
||||||
pub fn save_memory(ctx: &egui::Context) {
|
pub fn save_memory(ctx: &egui::Context) {
|
||||||
match ron::to_string(&*ctx.memory()) {
|
match ctx.memory(|mem| ron::to_string(mem)) {
|
||||||
Ok(ron) => {
|
Ok(ron) => {
|
||||||
local_storage_set("egui_memory_ron", &ron);
|
local_storage_set("egui_memory_ron", &ron);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ use wasm_bindgen::prelude::*;
|
||||||
static AGENT_ID: &str = "egui_text_agent";
|
static AGENT_ID: &str = "egui_text_agent";
|
||||||
|
|
||||||
pub fn text_agent() -> web_sys::HtmlInputElement {
|
pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
web_sys::window()
|
web_sys::window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.document()
|
.document()
|
||||||
|
@ -23,7 +22,6 @@ pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||||
|
|
||||||
/// Text event handler,
|
/// Text event handler,
|
||||||
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let window = web_sys::window().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
let body = document.body().expect("document should have a body");
|
let body = document.body().expect("document should have a body");
|
||||||
|
@ -129,7 +127,6 @@ pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(
|
||||||
|
|
||||||
/// Focus or blur text agent to toggle mobile keyboard.
|
/// Focus or blur text agent to toggle mobile keyboard.
|
||||||
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
|
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
let window = web_sys::window()?;
|
let window = web_sys::window()?;
|
||||||
let document = window.document()?;
|
let document = window.document()?;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use egui::Rgba;
|
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
/// Renderer for a browser canvas.
|
/// Renderer for a browser canvas.
|
||||||
|
@ -19,7 +18,7 @@ pub(crate) trait WebPainter {
|
||||||
/// Update all internal textures and paint gui.
|
/// Update all internal textures and paint gui.
|
||||||
fn paint_and_update_textures(
|
fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
clear_color: Rgba,
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &egui::TexturesDelta,
|
||||||
|
|
|
@ -2,7 +2,6 @@ use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
use egui::Rgba;
|
|
||||||
use egui_glow::glow;
|
use egui_glow::glow;
|
||||||
|
|
||||||
use crate::{WebGlContextOption, WebOptions};
|
use crate::{WebGlContextOption, WebOptions};
|
||||||
|
@ -49,7 +48,7 @@ impl WebPainter for WebPainterGlow {
|
||||||
|
|
||||||
fn paint_and_update_textures(
|
fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
clear_color: Rgba,
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &egui::TexturesDelta,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
use egui::{mutex::RwLock, Rgba};
|
use egui::mutex::RwLock;
|
||||||
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
||||||
|
|
||||||
use crate::WebOptions;
|
use crate::WebOptions;
|
||||||
|
@ -49,6 +49,7 @@ impl WebPainterWgpu {
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: depth_format,
|
format: depth_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[depth_format],
|
||||||
})
|
})
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
})
|
})
|
||||||
|
@ -60,8 +61,13 @@ impl WebPainterWgpu {
|
||||||
|
|
||||||
let canvas = super::canvas_element_or_die(canvas_id);
|
let canvas = super::canvas_element_or_die(canvas_id);
|
||||||
|
|
||||||
let instance = wgpu::Instance::new(options.wgpu_options.backends);
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
let surface = instance.create_surface_from_canvas(&canvas);
|
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
|
let adapter = instance
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
@ -81,7 +87,7 @@ impl WebPainterWgpu {
|
||||||
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
|
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
|
||||||
|
|
||||||
let target_format =
|
let target_format =
|
||||||
egui_wgpu::preferred_framebuffer_format(&surface.get_supported_formats(&adapter));
|
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
|
||||||
|
|
||||||
let depth_format = options.wgpu_options.depth_format;
|
let depth_format = options.wgpu_options.depth_format;
|
||||||
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
|
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
|
||||||
|
@ -99,6 +105,7 @@ impl WebPainterWgpu {
|
||||||
height: 0,
|
height: 0,
|
||||||
present_mode: options.wgpu_options.present_mode,
|
present_mode: options.wgpu_options.present_mode,
|
||||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||||
|
view_formats: vec![target_format],
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!("wgpu painter initialized.");
|
tracing::debug!("wgpu painter initialized.");
|
||||||
|
@ -128,7 +135,7 @@ impl WebPainter for WebPainterWgpu {
|
||||||
|
|
||||||
fn paint_and_update_textures(
|
fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
clear_color: Rgba,
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &egui::TexturesDelta,
|
||||||
|
@ -221,10 +228,10 @@ impl WebPainter for WebPainterWgpu {
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
r: clear_color.r() as f64,
|
r: clear_color[0] as f64,
|
||||||
g: clear_color.g() as f64,
|
g: clear_color[1] as f64,
|
||||||
b: clear_color.b() as f64,
|
b: clear_color[2] as f64,
|
||||||
a: clear_color.a() as f64,
|
a: clear_color[3] as f64,
|
||||||
}),
|
}),
|
||||||
store: true,
|
store: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,9 +3,14 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## 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)).
|
* 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)).
|
* `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)).
|
* `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
|
## 0.20.0 - 2022-12-08 - web support
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
description = "Bindings for using egui natively using the wgpu library"
|
description = "Bindings for using egui natively using the wgpu library"
|
||||||
authors = [
|
authors = [
|
||||||
"Nils Hasenbanck <nils@hasenbanck.de>",
|
"Nils Hasenbanck <nils@hasenbanck.de>",
|
||||||
|
@ -36,20 +36,20 @@ winit = ["dep:winit"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
epaint = { version = "0.20.0", path = "../epaint", default-features = false, features = [
|
epaint = { version = "0.21.0", path = "../epaint", default-features = false, features = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
bytemuck = "1.7"
|
bytemuck = "1.7"
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
type-map = "0.5.0"
|
type-map = "0.5.0"
|
||||||
wgpu = "0.14"
|
wgpu = "0.15.0"
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
winit = { version = "0.27.2", optional = true }
|
winit = { version = "0.28", optional = true }
|
||||||
|
|
||||||
# Native:
|
# Native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
|
|
@ -99,7 +99,39 @@ pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::Te
|
||||||
}
|
}
|
||||||
formats[0] // take the first
|
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"
|
/// Profiling macro for feature "puffin"
|
||||||
|
|
|
@ -554,6 +554,7 @@ impl Renderer {
|
||||||
dimension: wgpu::TextureDimension::D2,
|
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, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
||||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||||
});
|
});
|
||||||
let sampler = self
|
let sampler = self
|
||||||
.samplers
|
.samplers
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tracing::error;
|
|
||||||
use wgpu::{Adapter, Instance, Surface};
|
|
||||||
|
|
||||||
use epaint::mutex::RwLock;
|
use epaint::mutex::RwLock;
|
||||||
|
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
||||||
|
|
||||||
struct SurfaceState {
|
struct SurfaceState {
|
||||||
surface: Surface,
|
surface: wgpu::Surface,
|
||||||
|
alpha_mode: wgpu::CompositeAlphaMode,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,12 @@ struct SurfaceState {
|
||||||
pub struct Painter {
|
pub struct Painter {
|
||||||
configuration: WgpuConfiguration,
|
configuration: WgpuConfiguration,
|
||||||
msaa_samples: u32,
|
msaa_samples: u32,
|
||||||
|
support_transparent_backbuffer: bool,
|
||||||
depth_format: Option<wgpu::TextureFormat>,
|
depth_format: Option<wgpu::TextureFormat>,
|
||||||
depth_texture_view: Option<wgpu::TextureView>,
|
depth_texture_view: Option<wgpu::TextureView>,
|
||||||
|
|
||||||
instance: Instance,
|
instance: wgpu::Instance,
|
||||||
adapter: Option<Adapter>,
|
adapter: Option<wgpu::Adapter>,
|
||||||
render_state: Option<RenderState>,
|
render_state: Option<RenderState>,
|
||||||
surface_state: Option<SurfaceState>,
|
surface_state: Option<SurfaceState>,
|
||||||
}
|
}
|
||||||
|
@ -41,12 +42,21 @@ impl Painter {
|
||||||
/// [`set_window()`](Self::set_window) once you have
|
/// [`set_window()`](Self::set_window) once you have
|
||||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||||
/// associated.
|
/// associated.
|
||||||
pub fn new(configuration: WgpuConfiguration, msaa_samples: u32, depth_bits: u8) -> Self {
|
pub fn new(
|
||||||
let instance = wgpu::Instance::new(configuration.backends);
|
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 {
|
Self {
|
||||||
configuration,
|
configuration,
|
||||||
msaa_samples,
|
msaa_samples,
|
||||||
|
support_transparent_backbuffer,
|
||||||
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
|
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
|
||||||
depth_texture_view: None,
|
depth_texture_view: None,
|
||||||
|
|
||||||
|
@ -66,7 +76,7 @@ impl Painter {
|
||||||
|
|
||||||
async fn init_render_state(
|
async fn init_render_state(
|
||||||
&self,
|
&self,
|
||||||
adapter: &Adapter,
|
adapter: &wgpu::Adapter,
|
||||||
target_format: wgpu::TextureFormat,
|
target_format: wgpu::TextureFormat,
|
||||||
) -> Result<RenderState, wgpu::RequestDeviceError> {
|
) -> Result<RenderState, wgpu::RequestDeviceError> {
|
||||||
adapter
|
adapter
|
||||||
|
@ -91,7 +101,7 @@ impl Painter {
|
||||||
// will have the same format and so this render state will remain valid.
|
// will have the same format and so this render state will remain valid.
|
||||||
async fn ensure_render_state_for_surface(
|
async fn ensure_render_state_for_surface(
|
||||||
&mut self,
|
&mut self,
|
||||||
surface: &Surface,
|
surface: &wgpu::Surface,
|
||||||
) -> Result<(), wgpu::RequestDeviceError> {
|
) -> Result<(), wgpu::RequestDeviceError> {
|
||||||
if self.adapter.is_none() {
|
if self.adapter.is_none() {
|
||||||
self.adapter = self
|
self.adapter = self
|
||||||
|
@ -107,7 +117,7 @@ impl Painter {
|
||||||
match &self.adapter {
|
match &self.adapter {
|
||||||
Some(adapter) => {
|
Some(adapter) => {
|
||||||
let swapchain_format = crate::preferred_framebuffer_format(
|
let swapchain_format = crate::preferred_framebuffer_format(
|
||||||
&surface.get_supported_formats(adapter),
|
&surface.get_capabilities(adapter).formats,
|
||||||
);
|
);
|
||||||
let rs = self.init_render_state(adapter, swapchain_format).await?;
|
let rs = self.init_render_state(adapter, swapchain_format).await?;
|
||||||
self.render_state = Some(rs);
|
self.render_state = Some(rs);
|
||||||
|
@ -118,33 +128,23 @@ impl Painter {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
|
fn configure_surface(
|
||||||
crate::profile_function!();
|
surface_state: &SurfaceState,
|
||||||
|
render_state: &RenderState,
|
||||||
let render_state = self
|
present_mode: wgpu::PresentMode,
|
||||||
.render_state
|
) {
|
||||||
.as_ref()
|
surface_state.surface.configure(
|
||||||
.expect("Render state should exist before surface configuration");
|
&render_state.device,
|
||||||
let format = render_state.target_format;
|
&wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
let config = wgpu::SurfaceConfiguration {
|
format: render_state.target_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
width: surface_state.width,
|
||||||
format,
|
height: surface_state.height,
|
||||||
width: width_in_pixels,
|
present_mode,
|
||||||
height: height_in_pixels,
|
alpha_mode: surface_state.alpha_mode,
|
||||||
present_mode: self.configuration.present_mode,
|
view_formats: vec![render_state.target_format],
|
||||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
},
|
||||||
};
|
);
|
||||||
|
|
||||||
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`]
|
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
|
||||||
|
@ -177,22 +177,41 @@ impl Painter {
|
||||||
pub async unsafe fn set_window(
|
pub async unsafe fn set_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: Option<&winit::window::Window>,
|
window: Option<&winit::window::Window>,
|
||||||
) -> Result<(), wgpu::RequestDeviceError> {
|
) -> Result<(), crate::WgpuError> {
|
||||||
match window {
|
match window {
|
||||||
Some(window) => {
|
Some(window) => {
|
||||||
let surface = self.instance.create_surface(&window);
|
let surface = self.instance.create_surface(&window)?;
|
||||||
|
|
||||||
self.ensure_render_state_for_surface(&surface).await?;
|
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();
|
let size = window.inner_size();
|
||||||
let width = size.width;
|
|
||||||
let height = size.height;
|
|
||||||
self.surface_state = Some(SurfaceState {
|
self.surface_state = Some(SurfaceState {
|
||||||
surface,
|
surface,
|
||||||
width,
|
width: size.width,
|
||||||
height,
|
height: size.height,
|
||||||
|
alpha_mode,
|
||||||
});
|
});
|
||||||
self.resize_and_generate_depth_texture_view(width, height);
|
self.resize_and_generate_depth_texture_view(size.width, size.height);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.surface_state = None;
|
self.surface_state = None;
|
||||||
|
@ -217,10 +236,17 @@ impl Painter {
|
||||||
width_in_pixels: u32,
|
width_in_pixels: u32,
|
||||||
height_in_pixels: u32,
|
height_in_pixels: u32,
|
||||||
) {
|
) {
|
||||||
self.configure_surface(width_in_pixels, height_in_pixels);
|
let render_state = self.render_state.as_ref().unwrap();
|
||||||
let device = &self.render_state.as_ref().unwrap().device;
|
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| {
|
self.depth_texture_view = self.depth_format.map(|depth_format| {
|
||||||
device
|
render_state
|
||||||
|
.device
|
||||||
.create_texture(&wgpu::TextureDescriptor {
|
.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("egui_depth_texture"),
|
label: Some("egui_depth_texture"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
|
@ -234,6 +260,7 @@ impl Painter {
|
||||||
format: depth_format,
|
format: depth_format,
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[depth_format],
|
||||||
})
|
})
|
||||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||||
});
|
});
|
||||||
|
@ -250,7 +277,7 @@ impl Painter {
|
||||||
pub fn paint_and_update_textures(
|
pub fn paint_and_update_textures(
|
||||||
&mut self,
|
&mut self,
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
clear_color: epaint::Rgba,
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||||
textures_delta: &epaint::textures::TexturesDelta,
|
textures_delta: &epaint::textures::TexturesDelta,
|
||||||
) {
|
) {
|
||||||
|
@ -264,7 +291,6 @@ impl Painter {
|
||||||
Some(rs) => rs,
|
Some(rs) => rs,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let (width, height) = (surface_state.width, surface_state.height);
|
|
||||||
|
|
||||||
let output_frame = {
|
let output_frame = {
|
||||||
crate::profile_scope!("get_current_texture");
|
crate::profile_scope!("get_current_texture");
|
||||||
|
@ -277,7 +303,11 @@ impl Painter {
|
||||||
#[allow(clippy::single_match_else)]
|
#[allow(clippy::single_match_else)]
|
||||||
Err(e) => match (*self.configuration.on_surface_error)(e) {
|
Err(e) => match (*self.configuration.on_surface_error)(e) {
|
||||||
SurfaceErrorAction::RecreateSurface => {
|
SurfaceErrorAction::RecreateSurface => {
|
||||||
self.configure_surface(width, height);
|
Self::configure_surface(
|
||||||
|
surface_state,
|
||||||
|
render_state,
|
||||||
|
self.configuration.present_mode,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SurfaceErrorAction::SkipFrame => {
|
SurfaceErrorAction::SkipFrame => {
|
||||||
|
@ -295,7 +325,7 @@ impl Painter {
|
||||||
|
|
||||||
// Upload all resources for the GPU.
|
// Upload all resources for the GPU.
|
||||||
let screen_descriptor = renderer::ScreenDescriptor {
|
let screen_descriptor = renderer::ScreenDescriptor {
|
||||||
size_in_pixels: [width, height],
|
size_in_pixels: [surface_state.width, surface_state.height],
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -330,10 +360,10 @@ impl Painter {
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
r: clear_color.r() as f64,
|
r: clear_color[0] as f64,
|
||||||
g: clear_color.g() as f64,
|
g: clear_color[1] as f64,
|
||||||
b: clear_color.b() as f64,
|
b: clear_color[2] as f64,
|
||||||
a: clear_color.a() as f64,
|
a: clear_color[3] as f64,
|
||||||
}),
|
}),
|
||||||
store: true,
|
store: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,8 +5,19 @@ All notable changes to the `egui-winit` integration will be noted in this file.
|
||||||
## Unreleased
|
## 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
|
## 0.20.1 - 2022-12-11
|
||||||
* Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
* Fix [docs.rs](https://docs.rs/egui-winit) build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||||
|
|
||||||
|
|
||||||
## 0.20.0 - 2022-12-08
|
## 0.20.0 - 2022-12-08
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui-winit"
|
name = "egui-winit"
|
||||||
version = "0.20.1"
|
version = "0.21.1"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
description = "Bindings for using egui with winit"
|
description = "Bindings for using egui with winit"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -14,8 +14,7 @@ keywords = ["winit", "egui", "gui", "gamedev"]
|
||||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# Avoid speech-dispatcher dependencies - see https://docs.rs/crate/egui-winit/0.20.0/builds/695196
|
all-features = true
|
||||||
features = ["document-features"]
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -37,9 +36,6 @@ links = ["webbrowser"]
|
||||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||||
puffin = ["dep:puffin"]
|
puffin = ["dep:puffin"]
|
||||||
|
|
||||||
## Experimental support for a screen reader.
|
|
||||||
screen_reader = ["tts"]
|
|
||||||
|
|
||||||
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
|
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
|
||||||
serde = ["egui/serde", "dep:serde"]
|
serde = ["egui/serde", "dep:serde"]
|
||||||
|
|
||||||
|
@ -47,33 +43,34 @@ serde = ["egui/serde", "dep:serde"]
|
||||||
wayland = ["winit/wayland"]
|
wayland = ["winit/wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
|
egui = { version = "0.21.0", path = "../egui", default-features = false, features = [
|
||||||
"tracing",
|
"tracing",
|
||||||
] }
|
] }
|
||||||
instant = { version = "0.1", features = [
|
instant = { version = "0.1", features = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
] } # We use instant so we can (maybe) compile for web
|
] } # We use instant so we can (maybe) compile for web
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
winit = { version = "0.27.2", default-features = false }
|
winit = { version = "0.28", default-features = false }
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
|
||||||
|
# feature accesskit
|
||||||
|
accesskit_winit = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
||||||
# feature accesskit
|
|
||||||
accesskit_winit = { version = "0.7.1", optional = true }
|
|
||||||
|
|
||||||
puffin = { version = "0.14", optional = true }
|
puffin = { version = "0.14", optional = true }
|
||||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
# feature screen_reader
|
webbrowser = { version = "0.8.3", optional = true }
|
||||||
tts = { version = "0.24", optional = true }
|
|
||||||
|
|
||||||
webbrowser = { version = "0.8", optional = true }
|
|
||||||
|
|
||||||
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
|
[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 }
|
smithay-clipboard = { version = "0.6.3", optional = true }
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
arboard = { version = "3.2", optional = true, default-features = false }
|
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"] }
|
||||||
|
|
|
@ -19,23 +19,12 @@ use egui::accesskit;
|
||||||
pub use winit;
|
pub use winit;
|
||||||
|
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod screen_reader;
|
|
||||||
mod window_settings;
|
mod window_settings;
|
||||||
|
|
||||||
pub use window_settings::WindowSettings;
|
pub use window_settings::WindowSettings;
|
||||||
|
|
||||||
use winit::event_loop::EventLoopWindowTarget;
|
use winit::event_loop::EventLoopWindowTarget;
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
#[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 {
|
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
|
||||||
window.scale_factor() as f32
|
window.scale_factor() as f32
|
||||||
}
|
}
|
||||||
|
@ -70,12 +59,12 @@ pub struct State {
|
||||||
egui_input: egui::RawInput,
|
egui_input: egui::RawInput,
|
||||||
pointer_pos_in_points: Option<egui::Pos2>,
|
pointer_pos_in_points: Option<egui::Pos2>,
|
||||||
any_pointer_button_down: bool,
|
any_pointer_button_down: bool,
|
||||||
current_cursor_icon: egui::CursorIcon,
|
current_cursor_icon: Option<egui::CursorIcon>,
|
||||||
|
|
||||||
/// What egui uses.
|
/// What egui uses.
|
||||||
current_pixels_per_point: f32,
|
current_pixels_per_point: f32,
|
||||||
|
|
||||||
clipboard: clipboard::Clipboard,
|
clipboard: clipboard::Clipboard,
|
||||||
screen_reader: screen_reader::ScreenReader,
|
|
||||||
|
|
||||||
/// If `true`, mouse inputs will be treated as touches.
|
/// If `true`, mouse inputs will be treated as touches.
|
||||||
/// Useful for debugging touch support in egui.
|
/// Useful for debugging touch support in egui.
|
||||||
|
@ -111,11 +100,10 @@ impl State {
|
||||||
egui_input,
|
egui_input,
|
||||||
pointer_pos_in_points: None,
|
pointer_pos_in_points: None,
|
||||||
any_pointer_button_down: false,
|
any_pointer_button_down: false,
|
||||||
current_cursor_icon: egui::CursorIcon::Default,
|
current_cursor_icon: None,
|
||||||
current_pixels_per_point: 1.0,
|
current_pixels_per_point: 1.0,
|
||||||
|
|
||||||
clipboard: clipboard::Clipboard::new(wayland_display),
|
clipboard: clipboard::Clipboard::new(wayland_display),
|
||||||
screen_reader: screen_reader::ScreenReader::default(),
|
|
||||||
|
|
||||||
simulate_touch_screen: false,
|
simulate_touch_screen: false,
|
||||||
pointer_touch_id: None,
|
pointer_touch_id: None,
|
||||||
|
@ -380,8 +368,9 @@ impl State {
|
||||||
consumed: false,
|
consumed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::AxisMotion { .. }
|
|
||||||
| WindowEvent::CloseRequested
|
// Things that may require repaint:
|
||||||
|
WindowEvent::CloseRequested
|
||||||
| WindowEvent::CursorEntered { .. }
|
| WindowEvent::CursorEntered { .. }
|
||||||
| WindowEvent::Destroyed
|
| WindowEvent::Destroyed
|
||||||
| WindowEvent::Occluded(_)
|
| WindowEvent::Occluded(_)
|
||||||
|
@ -391,10 +380,26 @@ impl State {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
},
|
},
|
||||||
WindowEvent::Moved(_) => EventResponse {
|
|
||||||
repaint: false, // moving a window doesn't warrant a repaint
|
// Things we completely ignore:
|
||||||
|
WindowEvent::AxisMotion { .. }
|
||||||
|
| WindowEvent::Moved(_)
|
||||||
|
| WindowEvent::SmartMagnify { .. }
|
||||||
|
| WindowEvent::TouchpadRotate { .. } => EventResponse {
|
||||||
|
repaint: false,
|
||||||
consumed: 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,11 +620,6 @@ impl State {
|
||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
platform_output: egui::PlatformOutput,
|
platform_output: egui::PlatformOutput,
|
||||||
) {
|
) {
|
||||||
if egui_ctx.options().screen_reader {
|
|
||||||
self.screen_reader
|
|
||||||
.speak(&platform_output.events_description());
|
|
||||||
}
|
|
||||||
|
|
||||||
let egui::PlatformOutput {
|
let egui::PlatformOutput {
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
open_url,
|
open_url,
|
||||||
|
@ -655,22 +655,25 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
||||||
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
if self.current_cursor_icon == Some(cursor_icon) {
|
||||||
// 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.
|
||||||
if self.current_cursor_icon == cursor_icon {
|
// On other platforms: just early-out to save CPU.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.current_cursor_icon = cursor_icon;
|
|
||||||
|
|
||||||
if let Some(cursor_icon) = translate_cursor(cursor_icon) {
|
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
||||||
window.set_cursor_visible(true);
|
if is_pointer_in_window {
|
||||||
|
self.current_cursor_icon = Some(cursor_icon);
|
||||||
|
|
||||||
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
||||||
if is_pointer_in_window {
|
window.set_cursor_visible(true);
|
||||||
window.set_cursor_icon(cursor_icon);
|
window.set_cursor_icon(winit_cursor_icon);
|
||||||
|
} else {
|
||||||
|
window.set_cursor_visible(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.set_cursor_visible(false);
|
// Remember to set the cursor again once the cursor returns to the screen:
|
||||||
|
self.current_cursor_icon = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -879,6 +882,7 @@ fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_v
|
||||||
target_os = "openbsd"
|
target_os = "openbsd"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
|
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
|
||||||
return _event_loop.wayland_display();
|
return _event_loop.wayland_display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
pub struct ScreenReader {
|
|
||||||
#[cfg(feature = "tts")]
|
|
||||||
tts: Option<tts::Tts>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "tts"))]
|
|
||||||
#[allow(clippy::derivable_impls)] // False positive
|
|
||||||
impl Default for ScreenReader {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "tts")]
|
|
||||||
impl Default for ScreenReader {
|
|
||||||
fn default() -> Self {
|
|
||||||
let tts = match tts::Tts::default() {
|
|
||||||
Ok(screen_reader) => {
|
|
||||||
tracing::debug!("Initialized screen reader.");
|
|
||||||
Some(screen_reader)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!("Failed to load screen reader: {}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Self { tts }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScreenReader {
|
|
||||||
#[cfg(not(feature = "tts"))]
|
|
||||||
#[allow(clippy::unused_self)]
|
|
||||||
pub fn speak(&mut self, _text: &str) {}
|
|
||||||
|
|
||||||
#[cfg(feature = "tts")]
|
|
||||||
pub fn speak(&mut self, text: &str) {
|
|
||||||
if text.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(tts) = &mut self.tts {
|
|
||||||
tracing::debug!("Speaking: {:?}", text);
|
|
||||||
let interrupt = true;
|
|
||||||
if let Err(err) = tts.speak(text, interrupt) {
|
|
||||||
tracing::warn!("Failed to read: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,6 +42,10 @@ impl WindowSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
|
||||||
|
self.inner_size_points
|
||||||
|
}
|
||||||
|
|
||||||
pub fn initialize_window(
|
pub fn initialize_window(
|
||||||
&self,
|
&self,
|
||||||
mut window: winit::window::WindowBuilder,
|
mut window: winit::window::WindowBuilder,
|
||||||
|
@ -49,16 +53,13 @@ impl WindowSettings {
|
||||||
// If the app last ran on two monitors and only one is now connected, then
|
// If the app last ran on two monitors and only one is now connected, then
|
||||||
// the given position is invalid.
|
// the given position is invalid.
|
||||||
// If this happens on Mac, the window is clamped into valid area.
|
// 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.
|
// If this happens on Windows, the clamping behavior is managed by the function
|
||||||
// So we don't restore window positions on Windows.
|
// clamp_window_to_sane_position.
|
||||||
let try_restore_position = !cfg!(target_os = "windows");
|
if let Some(pos) = self.position {
|
||||||
if try_restore_position {
|
window = window.with_position(winit::dpi::PhysicalPosition {
|
||||||
if let Some(pos) = self.position {
|
x: pos.x as f64,
|
||||||
window = window.with_position(winit::dpi::PhysicalPosition {
|
y: pos.y as f64,
|
||||||
x: pos.x as f64,
|
});
|
||||||
y: pos.y as f64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(inner_size_points) = self.inner_size_points {
|
if let Some(inner_size_points) = self.inner_size_points {
|
||||||
|
@ -86,4 +87,58 @@ impl WindowSettings {
|
||||||
*size = size.at_most(max_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui"
|
name = "egui"
|
||||||
version = "0.20.1"
|
version = "0.21.0"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
description = "An easy-to-use immediate mode GUI that runs on both web and native"
|
description = "An easy-to-use immediate mode GUI that runs on both web and native"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -54,8 +54,12 @@ persistence = ["serde", "epaint/serde", "ron"]
|
||||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||||
|
|
||||||
|
## Change Vertex layout to be compatible with unity
|
||||||
|
unity = ["epaint/unity"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
epaint = { version = "0.20.0", path = "../epaint", default-features = false }
|
epaint = { version = "0.21.0", path = "../epaint", default-features = false }
|
||||||
|
|
||||||
ahash = { version = "0.8.1", default-features = false, features = [
|
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
|
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||||
|
@ -66,7 +70,7 @@ nohash-hasher = "0.2"
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
## Exposes detailed accessibility implementation required by platform
|
## Exposes detailed accessibility implementation required by platform
|
||||||
## accessibility APIs. Also requires support in the egui integration.
|
## accessibility APIs. Also requires support in the egui integration.
|
||||||
accesskit = { version = "0.8.1", optional = true }
|
accesskit = { version = "0.9.0", optional = true }
|
||||||
|
|
||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub(crate) struct AnimationManager {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct BoolAnim {
|
struct BoolAnim {
|
||||||
value: bool,
|
value: bool,
|
||||||
|
|
||||||
/// when did `value` last toggle?
|
/// when did `value` last toggle?
|
||||||
toggle_time: f64,
|
toggle_time: f64,
|
||||||
}
|
}
|
||||||
|
@ -16,7 +17,9 @@ struct BoolAnim {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct ValueAnim {
|
struct ValueAnim {
|
||||||
from_value: f32,
|
from_value: f32,
|
||||||
|
|
||||||
to_value: f32,
|
to_value: f32,
|
||||||
|
|
||||||
/// when did `value` last toggle?
|
/// when did `value` last toggle?
|
||||||
toggle_time: f64,
|
toggle_time: f64,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// State that is persisted between frames.
|
/// 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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub(crate) struct State {
|
pub(crate) struct State {
|
||||||
/// Last known pos
|
/// Last known pos of the pivot
|
||||||
pub pos: Pos2,
|
pub pivot_pos: Pos2,
|
||||||
|
|
||||||
|
pub pivot: Align2,
|
||||||
|
|
||||||
/// Last know size. Used for catching clicks.
|
/// Last know size. Used for catching clicks.
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
@ -21,8 +23,22 @@ pub(crate) struct State {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn rect(&self) -> Rect {
|
||||||
Rect::from_min_size(self.pos, self.size)
|
Rect::from_min_size(self.left_top_pos(), self.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,27 +247,25 @@ impl Area {
|
||||||
|
|
||||||
let layer_id = LayerId::new(order, id);
|
let layer_id = LayerId::new(order, id);
|
||||||
|
|
||||||
let state = ctx.memory().areas.get(id).copied();
|
let state = ctx.memory(|mem| mem.areas.get(id).copied());
|
||||||
let is_new = state.is_none();
|
let is_new = state.is_none();
|
||||||
if is_new {
|
if is_new {
|
||||||
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
|
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 {
|
let mut state = state.unwrap_or_else(|| State {
|
||||||
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
pivot_pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
||||||
|
pivot,
|
||||||
size: Vec2::ZERO,
|
size: Vec2::ZERO,
|
||||||
interactable,
|
interactable,
|
||||||
});
|
});
|
||||||
state.pos = new_pos.unwrap_or(state.pos);
|
state.pivot_pos = new_pos.unwrap_or(state.pivot_pos);
|
||||||
state.interactable = interactable;
|
state.interactable = interactable;
|
||||||
|
|
||||||
if pivot != Align2::LEFT_TOP {
|
|
||||||
state.pos.x -= pivot.x().to_factor() * state.size.x;
|
|
||||||
state.pos.y -= pivot.y().to_factor() * state.size.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((anchor, offset)) = anchor {
|
if let Some((anchor, offset)) = anchor {
|
||||||
let screen = ctx.available_rect();
|
let screen = ctx.available_rect();
|
||||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
state.set_left_top_pos(
|
||||||
|
anchor.align_size_within_rect(state.size, screen).left_top() + offset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// interact right away to prevent frame-delay
|
// interact right away to prevent frame-delay
|
||||||
|
@ -278,31 +292,33 @@ impl Area {
|
||||||
// Important check - don't try to move e.g. a combobox popup!
|
// Important check - don't try to move e.g. a combobox popup!
|
||||||
if movable {
|
if movable {
|
||||||
if move_response.dragged() {
|
if move_response.dragged() {
|
||||||
state.pos += ctx.input().pointer.delta();
|
state.pivot_pos += ctx.input(|i| i.pointer.delta());
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pos = ctx
|
state.set_left_top_pos(
|
||||||
.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||||
.min;
|
.min,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (move_response.dragged() || move_response.clicked())
|
if (move_response.dragged() || move_response.clicked())
|
||||||
|| pointer_pressed_on_area(ctx, layer_id)
|
|| pointer_pressed_on_area(ctx, layer_id)
|
||||||
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
|
||||||
{
|
{
|
||||||
ctx.memory().areas.move_to_top(layer_id);
|
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
move_response
|
move_response
|
||||||
};
|
};
|
||||||
|
|
||||||
state.pos = ctx.round_pos_to_pixels(state.pos);
|
state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos()));
|
||||||
|
|
||||||
if constrain {
|
if constrain {
|
||||||
state.pos = ctx
|
state.set_left_top_pos(
|
||||||
.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds)
|
||||||
.min;
|
.left_top(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
|
@ -329,7 +345,7 @@ impl Area {
|
||||||
}
|
}
|
||||||
|
|
||||||
let layer_id = LayerId::new(self.order, self.id);
|
let layer_id = LayerId::new(self.order, self.id);
|
||||||
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
|
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
|
||||||
if let Some(area_rect) = area_rect {
|
if let Some(area_rect) = area_rect {
|
||||||
let clip_rect = ctx.available_rect();
|
let clip_rect = ctx.available_rect();
|
||||||
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
|
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
|
||||||
|
@ -358,7 +374,7 @@ impl Prepared {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||||
let screen_rect = ctx.input().screen_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
|
|
||||||
let bounds = if let Some(bounds) = self.drag_bounds {
|
let bounds = if let Some(bounds) = self.drag_bounds {
|
||||||
bounds.intersect(screen_rect) // protect against infinite bounds
|
bounds.intersect(screen_rect) // protect against infinite bounds
|
||||||
|
@ -374,14 +390,16 @@ impl Prepared {
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_rect = Rect::from_min_max(
|
let max_rect = Rect::from_min_max(
|
||||||
self.state.pos,
|
self.state.left_top_pos(),
|
||||||
bounds.max.at_least(self.state.pos + Vec2::splat(32.0)),
|
bounds
|
||||||
|
.max
|
||||||
|
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
|
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_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
|
||||||
|
|
||||||
let clip_rect = Rect::from_min_max(self.state.pos, bounds.max)
|
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max)
|
||||||
.expand(clip_rect_margin)
|
.expand(clip_rect_margin)
|
||||||
.intersect(bounds);
|
.intersect(bounds);
|
||||||
|
|
||||||
|
@ -410,7 +428,7 @@ impl Prepared {
|
||||||
|
|
||||||
state.size = content_ui.min_rect().size();
|
state.size = content_ui.min_rect().size();
|
||||||
|
|
||||||
ctx.memory().areas.set_state(layer_id, state);
|
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
|
||||||
|
|
||||||
move_response
|
move_response
|
||||||
}
|
}
|
||||||
|
@ -418,7 +436,7 @@ impl Prepared {
|
||||||
|
|
||||||
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
||||||
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
||||||
let any_pressed = ctx.input().pointer.any_pressed();
|
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
|
||||||
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
|
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -426,13 +444,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn automatic_area_position(ctx: &Context) -> Pos2 {
|
fn automatic_area_position(ctx: &Context) -> Pos2 {
|
||||||
let mut existing: Vec<Rect> = ctx
|
let mut existing: Vec<Rect> = ctx.memory(|mem| {
|
||||||
.memory()
|
mem.areas
|
||||||
.areas
|
.visible_windows()
|
||||||
.visible_windows()
|
.into_iter()
|
||||||
.into_iter()
|
.map(State::rect)
|
||||||
.map(State::rect)
|
.collect()
|
||||||
.collect();
|
});
|
||||||
existing.sort_by_key(|r| r.left().round() as i32);
|
existing.sort_by_key(|r| r.left().round() as i32);
|
||||||
|
|
||||||
let available_rect = ctx.available_rect();
|
let available_rect = ctx.available_rect();
|
||||||
|
|
|
@ -26,13 +26,14 @@ pub struct CollapsingState {
|
||||||
|
|
||||||
impl CollapsingState {
|
impl CollapsingState {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data()
|
ctx.data_mut(|d| {
|
||||||
.get_persisted::<InnerState>(id)
|
d.get_persisted::<InnerState>(id)
|
||||||
.map(|state| Self { id, state })
|
.map(|state| Self { id, state })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(&self, ctx: &Context) {
|
pub fn store(&self, ctx: &Context) {
|
||||||
ctx.data().insert_persisted(self.id, self.state);
|
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn id(&self) -> Id {
|
pub fn id(&self) -> Id {
|
||||||
|
@ -64,7 +65,7 @@ impl CollapsingState {
|
||||||
|
|
||||||
/// 0 for closed, 1 for open, with tweening
|
/// 0 for closed, 1 for open, with tweening
|
||||||
pub fn openness(&self, ctx: &Context) -> f32 {
|
pub fn openness(&self, ctx: &Context) -> f32 {
|
||||||
if ctx.memory().everything_is_visible() {
|
if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||||
1.0
|
1.0
|
||||||
} else {
|
} else {
|
||||||
ctx.animate_bool(self.id, self.state.open)
|
ctx.animate_bool(self.id, self.state.open)
|
||||||
|
@ -111,10 +112,7 @@ impl CollapsingState {
|
||||||
response.rect.center().y,
|
response.rect.center().y,
|
||||||
));
|
));
|
||||||
let openness = self.openness(ui.ctx());
|
let openness = self.openness(ui.ctx());
|
||||||
let small_icon_response = Response {
|
let small_icon_response = response.clone().with_new_rect(icon_rect);
|
||||||
rect: icon_rect,
|
|
||||||
..response.clone()
|
|
||||||
};
|
|
||||||
icon_fn(ui, openness, &small_icon_response);
|
icon_fn(ui, openness, &small_icon_response);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
@ -143,9 +141,10 @@ impl CollapsingState {
|
||||||
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
|
add_header: impl FnOnce(&mut Ui) -> HeaderRet,
|
||||||
) -> HeaderResponse<'_, HeaderRet> {
|
) -> HeaderResponse<'_, HeaderRet> {
|
||||||
let header_response = ui.horizontal(|ui| {
|
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
|
ui.spacing_mut().item_spacing.x = 0.0; // the toggler button uses the full indent width
|
||||||
let collapser = self.show_default_button_indented(ui);
|
let collapser = self.show_default_button_indented(ui);
|
||||||
ui.spacing_mut().item_spacing.x = ui.spacing_mut().icon_spacing; // Restore spacing
|
ui.spacing_mut().item_spacing = prev_item_spacing;
|
||||||
(collapser, add_header(ui))
|
(collapser, add_header(ui))
|
||||||
});
|
});
|
||||||
HeaderResponse {
|
HeaderResponse {
|
||||||
|
@ -555,7 +554,7 @@ impl CollapsingHeader {
|
||||||
ui.painter().add(epaint::RectShape {
|
ui.painter().add(epaint::RectShape {
|
||||||
rect: header_response.rect.expand(visuals.expansion),
|
rect: header_response.rect.expand(visuals.expansion),
|
||||||
rounding: visuals.rounding,
|
rounding: visuals.rounding,
|
||||||
fill: visuals.bg_fill,
|
fill: visuals.weak_bg_fill,
|
||||||
stroke: visuals.bg_stroke,
|
stroke: visuals.bg_stroke,
|
||||||
// stroke: Default::default(),
|
// stroke: Default::default(),
|
||||||
});
|
});
|
||||||
|
@ -575,10 +574,7 @@ impl CollapsingHeader {
|
||||||
header_response.rect.left() + ui.spacing().indent / 2.0,
|
header_response.rect.left() + ui.spacing().indent / 2.0,
|
||||||
header_response.rect.center().y,
|
header_response.rect.center().y,
|
||||||
));
|
));
|
||||||
let icon_response = Response {
|
let icon_response = header_response.clone().with_new_rect(icon_rect);
|
||||||
rect: icon_rect,
|
|
||||||
..header_response.clone()
|
|
||||||
};
|
|
||||||
if let Some(icon) = icon {
|
if let Some(icon) = icon {
|
||||||
icon(ui, openness, &icon_response);
|
icon(ui, openness, &icon_response);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -162,9 +162,6 @@ impl ComboBox {
|
||||||
let button_id = ui.make_persistent_id(id_source);
|
let button_id = ui.make_persistent_id(id_source);
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(width) = width {
|
|
||||||
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
|
|
||||||
}
|
|
||||||
let mut ir = combo_box_dyn(
|
let mut ir = combo_box_dyn(
|
||||||
ui,
|
ui,
|
||||||
button_id,
|
button_id,
|
||||||
|
@ -172,6 +169,7 @@ impl ComboBox {
|
||||||
menu_contents,
|
menu_contents,
|
||||||
icon,
|
icon,
|
||||||
wrap_enabled,
|
wrap_enabled,
|
||||||
|
width,
|
||||||
);
|
);
|
||||||
if let Some(label) = label {
|
if let Some(label) = label {
|
||||||
ir.response
|
ir.response
|
||||||
|
@ -240,21 +238,17 @@ fn combo_box_dyn<'c, R>(
|
||||||
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||||
icon: Option<IconPainter>,
|
icon: Option<IconPainter>,
|
||||||
wrap_enabled: bool,
|
wrap_enabled: bool,
|
||||||
|
width: Option<f32>,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
let popup_id = button_id.with("popup");
|
let popup_id = button_id.with("popup");
|
||||||
|
|
||||||
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||||
|
|
||||||
let popup_height = ui
|
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
|
||||||
.ctx()
|
|
||||||
.memory()
|
|
||||||
.areas
|
|
||||||
.get(popup_id)
|
|
||||||
.map_or(100.0, |state| state.size.y);
|
|
||||||
|
|
||||||
let above_or_below =
|
let above_or_below =
|
||||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
||||||
< ui.ctx().input().screen_rect().bottom()
|
< ui.ctx().screen_rect().bottom()
|
||||||
{
|
{
|
||||||
AboveOrBelow::Below
|
AboveOrBelow::Below
|
||||||
} else {
|
} else {
|
||||||
|
@ -263,18 +257,20 @@ fn combo_box_dyn<'c, R>(
|
||||||
|
|
||||||
let margin = ui.spacing().button_padding;
|
let margin = ui.spacing().button_padding;
|
||||||
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
|
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
|
// We don't want to change width when user selects something new
|
||||||
let full_minimum_width = if wrap_enabled {
|
let full_minimum_width = if wrap_enabled {
|
||||||
// Currently selected value's text will be wrapped if needed, so occupy the available width.
|
// Currently selected value's text will be wrapped if needed, so occupy the available width.
|
||||||
ui.available_width()
|
ui.available_width()
|
||||||
} else {
|
} else {
|
||||||
// Occupy at least the minimum width assigned to Slider and ComboBox.
|
// Occupy at least the minimum width assigned to ComboBox.
|
||||||
ui.spacing().slider_width - 2.0 * margin.x
|
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
|
||||||
|
width - 2.0 * margin.x
|
||||||
};
|
};
|
||||||
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
||||||
let wrap_width = if wrap_enabled {
|
let wrap_width = if wrap_enabled {
|
||||||
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
|
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
|
||||||
ui.available_width() - ui.spacing().item_spacing.x - icon_size.x
|
ui.available_width() - icon_spacing - icon_size.x
|
||||||
} else {
|
} else {
|
||||||
// Use all the width necessary to display the currently selected value's text.
|
// Use all the width necessary to display the currently selected value's text.
|
||||||
f32::INFINITY
|
f32::INFINITY
|
||||||
|
@ -288,7 +284,7 @@ fn combo_box_dyn<'c, R>(
|
||||||
full_minimum_width
|
full_minimum_width
|
||||||
} else {
|
} else {
|
||||||
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
|
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
|
||||||
galley.size().x + ui.spacing().item_spacing.x + icon_size.x
|
galley.size().x + icon_spacing + icon_size.x
|
||||||
};
|
};
|
||||||
|
|
||||||
// Case : wrap_enabled : occupy all the available width.
|
// Case : wrap_enabled : occupy all the available width.
|
||||||
|
@ -333,7 +329,7 @@ fn combo_box_dyn<'c, R>(
|
||||||
});
|
});
|
||||||
|
|
||||||
if button_response.clicked() {
|
if button_response.clicked() {
|
||||||
ui.memory().toggle_popup(popup_id);
|
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||||
}
|
}
|
||||||
let inner = crate::popup::popup_above_or_below_widget(
|
let inner = crate::popup::popup_above_or_below_widget(
|
||||||
ui,
|
ui,
|
||||||
|
@ -390,7 +386,7 @@ fn button_frame(
|
||||||
epaint::RectShape {
|
epaint::RectShape {
|
||||||
rect: outer_rect.expand(visuals.expansion),
|
rect: outer_rect.expand(visuals.expansion),
|
||||||
rounding: visuals.rounding,
|
rounding: visuals.rounding,
|
||||||
fill: visuals.bg_fill,
|
fill: visuals.weak_bg_fill,
|
||||||
stroke: visuals.bg_stroke,
|
stroke: visuals.bg_stroke,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,11 +19,16 @@ use epaint::*;
|
||||||
pub struct Frame {
|
pub struct Frame {
|
||||||
/// Margin within the painted frame.
|
/// Margin within the painted frame.
|
||||||
pub inner_margin: Margin,
|
pub inner_margin: Margin,
|
||||||
|
|
||||||
/// Margin outside the painted frame.
|
/// Margin outside the painted frame.
|
||||||
pub outer_margin: Margin,
|
pub outer_margin: Margin,
|
||||||
|
|
||||||
pub rounding: Rounding,
|
pub rounding: Rounding,
|
||||||
|
|
||||||
pub shadow: Shadow,
|
pub shadow: Shadow,
|
||||||
|
|
||||||
pub fill: Color32,
|
pub fill: Color32,
|
||||||
|
|
||||||
pub stroke: Stroke,
|
pub stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub struct PanelState {
|
||||||
|
|
||||||
impl PanelState {
|
impl PanelState {
|
||||||
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
|
||||||
ctx.data().get_persisted(bar_id)
|
ctx.data_mut(|d| d.get_persisted(bar_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The size of the panel (from previous frame).
|
/// The size of the panel (from previous frame).
|
||||||
|
@ -37,7 +37,7 @@ impl PanelState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ctx: &Context, bar_id: Id) {
|
fn store(self, ctx: &Context, bar_id: Id) {
|
||||||
ctx.data().insert_persisted(bar_id, self);
|
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,11 +245,12 @@ impl SidePanel {
|
||||||
&& (resize_x - pointer.x).abs()
|
&& (resize_x - pointer.x).abs()
|
||||||
<= ui.style().interaction.resize_grab_radius_side;
|
<= ui.style().interaction.resize_grab_radius_side;
|
||||||
|
|
||||||
let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks
|
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
||||||
if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line {
|
&& mouse_over_resize_line
|
||||||
ui.memory().set_dragged_id(resize_id);
|
{
|
||||||
|
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
|
||||||
}
|
}
|
||||||
is_resizing = ui.memory().is_being_dragged(resize_id);
|
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
|
||||||
if is_resizing {
|
if is_resizing {
|
||||||
let width = (pointer.x - side.side_x(panel_rect)).abs();
|
let width = (pointer.x - side.side_x(panel_rect)).abs();
|
||||||
let width =
|
let width =
|
||||||
|
@ -257,12 +258,12 @@ impl SidePanel {
|
||||||
side.set_rect_width(&mut panel_rect, width);
|
side.set_rect_width(&mut panel_rect, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
let dragging_something_else =
|
||||||
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
|
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
||||||
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||||
|
|
||||||
if resize_hover || is_resizing {
|
if resize_hover || is_resizing {
|
||||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,12 +297,12 @@ impl SidePanel {
|
||||||
|
|
||||||
{
|
{
|
||||||
let stroke = if is_resizing {
|
let stroke = if is_resizing {
|
||||||
ui.style().visuals.widgets.active.bg_stroke
|
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||||
} else if resize_hover {
|
} else if resize_hover {
|
||||||
ui.style().visuals.widgets.hovered.bg_stroke
|
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||||
} else if show_separator_line {
|
} else if show_separator_line {
|
||||||
// TOOD(emilk): distinguish resizable from non-resizable
|
// TOOD(emilk): distinguish resizable from non-resizable
|
||||||
ui.style().visuals.widgets.noninteractive.bg_stroke
|
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
};
|
};
|
||||||
|
@ -334,19 +335,19 @@ impl SidePanel {
|
||||||
let layer_id = LayerId::background();
|
let layer_id = LayerId::background();
|
||||||
let side = self.side;
|
let side = self.side;
|
||||||
let available_rect = ctx.available_rect();
|
let available_rect = ctx.available_rect();
|
||||||
let clip_rect = ctx.input().screen_rect();
|
let clip_rect = ctx.screen_rect();
|
||||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_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 inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
|
||||||
let rect = inner_response.response.rect;
|
let rect = inner_response.response.rect;
|
||||||
|
|
||||||
match side {
|
match side {
|
||||||
Side::Left => ctx
|
Side::Left => ctx.frame_state_mut(|state| {
|
||||||
.frame_state()
|
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||||
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
|
}),
|
||||||
Side::Right => ctx
|
Side::Right => ctx.frame_state_mut(|state| {
|
||||||
.frame_state()
|
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||||
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
|
}),
|
||||||
}
|
}
|
||||||
inner_response
|
inner_response
|
||||||
}
|
}
|
||||||
|
@ -682,7 +683,7 @@ impl TopBottomPanel {
|
||||||
let mut is_resizing = false;
|
let mut is_resizing = false;
|
||||||
if resizable {
|
if resizable {
|
||||||
let resize_id = id.with("__resize");
|
let resize_id = id.with("__resize");
|
||||||
let latest_pos = ui.input().pointer.latest_pos();
|
let latest_pos = ui.input(|i| i.pointer.latest_pos());
|
||||||
if let Some(pointer) = latest_pos {
|
if let Some(pointer) = latest_pos {
|
||||||
let we_are_on_top = ui
|
let we_are_on_top = ui
|
||||||
.ctx()
|
.ctx()
|
||||||
|
@ -695,13 +696,12 @@ impl TopBottomPanel {
|
||||||
&& (resize_y - pointer.y).abs()
|
&& (resize_y - pointer.y).abs()
|
||||||
<= ui.style().interaction.resize_grab_radius_side;
|
<= ui.style().interaction.resize_grab_radius_side;
|
||||||
|
|
||||||
if ui.input().pointer.any_pressed()
|
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
||||||
&& ui.input().pointer.any_down()
|
|
||||||
&& mouse_over_resize_line
|
&& mouse_over_resize_line
|
||||||
{
|
{
|
||||||
ui.memory().interaction.drag_id = Some(resize_id);
|
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id));
|
||||||
}
|
}
|
||||||
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
|
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
|
||||||
if is_resizing {
|
if is_resizing {
|
||||||
let height = (pointer.y - side.side_y(panel_rect)).abs();
|
let height = (pointer.y - side.side_y(panel_rect)).abs();
|
||||||
let height = clamp_to_range(height, height_range.clone())
|
let height = clamp_to_range(height, height_range.clone())
|
||||||
|
@ -709,12 +709,12 @@ impl TopBottomPanel {
|
||||||
side.set_rect_height(&mut panel_rect, height);
|
side.set_rect_height(&mut panel_rect, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
let dragging_something_else =
|
||||||
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
|
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
||||||
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||||
|
|
||||||
if resize_hover || is_resizing {
|
if resize_hover || is_resizing {
|
||||||
ui.output().cursor_icon = CursorIcon::ResizeVertical;
|
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -748,12 +748,12 @@ impl TopBottomPanel {
|
||||||
|
|
||||||
{
|
{
|
||||||
let stroke = if is_resizing {
|
let stroke = if is_resizing {
|
||||||
ui.style().visuals.widgets.active.bg_stroke
|
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||||
} else if resize_hover {
|
} else if resize_hover {
|
||||||
ui.style().visuals.widgets.hovered.bg_stroke
|
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||||
} else if show_separator_line {
|
} else if show_separator_line {
|
||||||
// TOOD(emilk): distinguish resizable from non-resizable
|
// TOOD(emilk): distinguish resizable from non-resizable
|
||||||
ui.style().visuals.widgets.noninteractive.bg_stroke
|
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
};
|
};
|
||||||
|
@ -787,7 +787,7 @@ impl TopBottomPanel {
|
||||||
let available_rect = ctx.available_rect();
|
let available_rect = ctx.available_rect();
|
||||||
let side = self.side;
|
let side = self.side;
|
||||||
|
|
||||||
let clip_rect = ctx.input().screen_rect();
|
let clip_rect = ctx.screen_rect();
|
||||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_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 inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
|
||||||
|
@ -795,12 +795,14 @@ impl TopBottomPanel {
|
||||||
|
|
||||||
match side {
|
match side {
|
||||||
TopBottomSide::Top => {
|
TopBottomSide::Top => {
|
||||||
ctx.frame_state()
|
ctx.frame_state_mut(|state| {
|
||||||
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
TopBottomSide::Bottom => {
|
TopBottomSide::Bottom => {
|
||||||
ctx.frame_state()
|
ctx.frame_state_mut(|state| {
|
||||||
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1042,14 +1044,13 @@ impl CentralPanel {
|
||||||
let layer_id = LayerId::background();
|
let layer_id = LayerId::background();
|
||||||
let id = Id::new("central_panel");
|
let id = Id::new("central_panel");
|
||||||
|
|
||||||
let clip_rect = ctx.input().screen_rect();
|
let clip_rect = ctx.screen_rect();
|
||||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_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);
|
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.
|
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
|
||||||
ctx.frame_state()
|
ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
|
||||||
.allocate_central_panel(inner_response.response.rect);
|
|
||||||
|
|
||||||
inner_response
|
inner_response
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,11 @@ pub(crate) struct TooltipState {
|
||||||
|
|
||||||
impl TooltipState {
|
impl TooltipState {
|
||||||
pub fn load(ctx: &Context) -> Option<Self> {
|
pub fn load(ctx: &Context) -> Option<Self> {
|
||||||
ctx.data().get_temp(Id::null())
|
ctx.data_mut(|d| d.get_temp(Id::null()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ctx: &Context) {
|
fn store(self, ctx: &Context) {
|
||||||
ctx.data().insert_temp(Id::null(), self);
|
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
||||||
|
@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
let suggested_pos = ctx
|
let suggested_pos = ctx
|
||||||
.input()
|
.input(|i| i.pointer.hover_pos())
|
||||||
.pointer
|
|
||||||
.hover_pos()
|
|
||||||
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
||||||
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
||||||
}
|
}
|
||||||
|
@ -112,7 +110,7 @@ pub fn show_tooltip_for<R>(
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
||||||
let (above, position) = if ctx.input().any_touches() {
|
let (above, position) = if ctx.input(|i| i.any_touches()) {
|
||||||
(true, expanded_rect.left_top())
|
(true, expanded_rect.left_top())
|
||||||
} else {
|
} else {
|
||||||
(false, expanded_rect.left_bottom())
|
(false, expanded_rect.left_bottom())
|
||||||
|
@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
|
|
||||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
||||||
let mut frame_state =
|
let mut frame_state =
|
||||||
ctx.frame_state()
|
ctx.frame_state(|fs| fs.tooltip_state)
|
||||||
.tooltip_state
|
|
||||||
.unwrap_or(crate::frame_state::TooltipFrameState {
|
.unwrap_or(crate::frame_state::TooltipFrameState {
|
||||||
common_id: individual_id,
|
common_id: individual_id,
|
||||||
rect: Rect::NOTHING,
|
rect: Rect::NOTHING,
|
||||||
|
@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
}
|
}
|
||||||
} else if let Some(position) = suggested_position {
|
} else if let Some(position) = suggested_position {
|
||||||
position
|
position
|
||||||
} else if ctx.memory().everything_is_visible() {
|
} else if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||||
Pos2::ZERO
|
Pos2::ZERO
|
||||||
} else {
|
} else {
|
||||||
return None; // No good place for a tooltip :(
|
return None; // No good place for a tooltip :(
|
||||||
|
@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
position.y -= expected_size.y;
|
position.y -= expected_size.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
position = position.at_most(ctx.input().screen_rect().max - expected_size);
|
position = position.at_most(ctx.screen_rect().max - expected_size);
|
||||||
|
|
||||||
// check if we intersect the avoid_rect
|
// check if we intersect the avoid_rect
|
||||||
{
|
{
|
||||||
|
@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let position = position.at_least(ctx.input().screen_rect().min);
|
let position = position.at_least(ctx.screen_rect().min);
|
||||||
|
|
||||||
let area_id = frame_state.common_id.with(frame_state.count);
|
let area_id = frame_state.common_id.with(frame_state.count);
|
||||||
|
|
||||||
|
@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||||
|
|
||||||
frame_state.count += 1;
|
frame_state.count += 1;
|
||||||
frame_state.rect = frame_state.rect.union(response.rect);
|
frame_state.rect = frame_state.rect.union(response.rect);
|
||||||
ctx.frame_state().tooltip_state = Some(frame_state);
|
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
|
||||||
|
|
||||||
Some(inner)
|
Some(inner)
|
||||||
}
|
}
|
||||||
|
@ -263,8 +260,9 @@ fn show_tooltip_area_dyn<'c, R>(
|
||||||
Area::new(area_id)
|
Area::new(area_id)
|
||||||
.order(Order::Tooltip)
|
.order(Order::Tooltip)
|
||||||
.fixed_pos(window_pos)
|
.fixed_pos(window_pos)
|
||||||
|
.constrain(true)
|
||||||
.interactable(false)
|
.interactable(false)
|
||||||
.drag_bounds(Rect::EVERYTHING) // disable clip rect
|
.drag_bounds(ctx.screen_rect())
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
Frame::popup(&ctx.style())
|
Frame::popup(&ctx.style())
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
@ -283,7 +281,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||||
if *individual_id == tooltip_id {
|
if *individual_id == tooltip_id {
|
||||||
let area_id = common_id.with(count);
|
let area_id = common_id.with(count);
|
||||||
let layer_id = LayerId::new(Order::Tooltip, area_id);
|
let layer_id = LayerId::new(Order::Tooltip, area_id);
|
||||||
if ctx.memory().areas.visible_last_frame(&layer_id) {
|
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,6 +312,8 @@ pub fn popup_below_widget<R>(
|
||||||
///
|
///
|
||||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
/// 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`].
|
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
|
||||||
///
|
///
|
||||||
/// Returns `None` if the popup is not open.
|
/// Returns `None` if the popup is not open.
|
||||||
|
@ -323,7 +323,7 @@ pub fn popup_below_widget<R>(
|
||||||
/// let response = ui.button("Open popup");
|
/// let response = ui.button("Open popup");
|
||||||
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
||||||
/// if response.clicked() {
|
/// if response.clicked() {
|
||||||
/// ui.memory().toggle_popup(popup_id);
|
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||||
/// }
|
/// }
|
||||||
/// let below = egui::AboveOrBelow::Below;
|
/// let below = egui::AboveOrBelow::Below;
|
||||||
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
|
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
|
||||||
|
@ -340,7 +340,7 @@ pub fn popup_above_or_below_widget<R>(
|
||||||
above_or_below: AboveOrBelow,
|
above_or_below: AboveOrBelow,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
if ui.memory().is_popup_open(popup_id) {
|
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||||
let (pos, pivot) = match above_or_below {
|
let (pos, pivot) = match above_or_below {
|
||||||
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
||||||
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
||||||
|
@ -368,8 +368,8 @@ pub fn popup_above_or_below_widget<R>(
|
||||||
})
|
})
|
||||||
.inner;
|
.inner;
|
||||||
|
|
||||||
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
|
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
|
||||||
ui.memory().close_popup();
|
ui.memory_mut(|mem| mem.close_popup());
|
||||||
}
|
}
|
||||||
Some(inner)
|
Some(inner)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -18,11 +18,11 @@ pub(crate) struct State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data().get_persisted(id)
|
ctx.data_mut(|d| d.get_persisted(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data().insert_persisted(id, self);
|
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ impl Resize {
|
||||||
.at_least(self.min_size)
|
.at_least(self.min_size)
|
||||||
.at_most(self.max_size)
|
.at_most(self.max_size)
|
||||||
.at_most(
|
.at_most(
|
||||||
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||||
);
|
);
|
||||||
|
|
||||||
State {
|
State {
|
||||||
|
@ -305,7 +305,7 @@ impl Resize {
|
||||||
paint_resize_corner(ui, &corner_response);
|
paint_resize_corner(ui, &corner_response);
|
||||||
|
|
||||||
if corner_response.hovered() || corner_response.dragged() {
|
if corner_response.hovered() || corner_response.dragged() {
|
||||||
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
|
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,11 +48,11 @@ impl Default for State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data().get_persisted(id)
|
ctx.data_mut(|d| d.get_persisted(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data().insert_persisted(id, self);
|
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,15 +426,34 @@ impl ScrollArea {
|
||||||
|
|
||||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
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());
|
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
|
||||||
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:
|
// Clip the content, but only when we really need to:
|
||||||
for d in 0..2 {
|
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
||||||
if !has_bar[d] {
|
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
|
||||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
let mut content_clip_rect = ui.clip_rect();
|
||||||
|
for d in 0..2 {
|
||||||
|
if has_bar[d] {
|
||||||
|
if state.content_is_too_large[d] {
|
||||||
|
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||||
|
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.show_scroll[d] {
|
||||||
|
// Make sure content doesn't cover scroll bars
|
||||||
|
let tiny_gap = 1.0;
|
||||||
|
content_clip_rect.max[1 - d] =
|
||||||
|
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nice handling of forced resizing beyond the possible:
|
||||||
|
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Make sure we din't accidentally expand the clip rect
|
||||||
|
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||||
|
content_ui.set_clip_rect(content_clip_rect);
|
||||||
}
|
}
|
||||||
content_ui.set_clip_rect(content_clip_rect);
|
|
||||||
|
|
||||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||||
|
|
||||||
|
@ -449,8 +468,10 @@ impl ScrollArea {
|
||||||
if content_response.dragged() {
|
if content_response.dragged() {
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if has_bar[d] {
|
if has_bar[d] {
|
||||||
state.offset[d] -= ui.input().pointer.delta()[d];
|
ui.input(|input| {
|
||||||
state.vel[d] = ui.input().pointer.velocity()[d];
|
state.offset[d] -= input.pointer.delta()[d];
|
||||||
|
state.vel[d] = input.pointer.velocity()[d];
|
||||||
|
});
|
||||||
state.scroll_stuck_to_end[d] = false;
|
state.scroll_stuck_to_end[d] = false;
|
||||||
} else {
|
} else {
|
||||||
state.vel[d] = 0.0;
|
state.vel[d] = 0.0;
|
||||||
|
@ -459,7 +480,7 @@ impl ScrollArea {
|
||||||
} else {
|
} else {
|
||||||
let stop_speed = 20.0; // Pixels per second.
|
let stop_speed = 20.0; // Pixels per second.
|
||||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||||
let dt = ui.input().unstable_dt;
|
let dt = ui.input(|i| i.unstable_dt);
|
||||||
|
|
||||||
let friction = friction_coeff * dt;
|
let friction = friction_coeff * dt;
|
||||||
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
||||||
|
@ -603,7 +624,9 @@ impl Prepared {
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if has_bar[d] {
|
if has_bar[d] {
|
||||||
// We take the scroll target so only this ScrollArea will use it:
|
// We take the scroll target so only this ScrollArea will use it:
|
||||||
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
let scroll_target = content_ui
|
||||||
|
.ctx()
|
||||||
|
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||||
if let Some((scroll, align)) = scroll_target {
|
if let Some((scroll, align)) = scroll_target {
|
||||||
let min = content_ui.min_rect().min[d];
|
let min = content_ui.min_rect().min[d];
|
||||||
let clip_rect = content_ui.clip_rect();
|
let clip_rect = content_ui.clip_rect();
|
||||||
|
@ -668,8 +691,7 @@ impl Prepared {
|
||||||
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
|
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if has_bar[d] {
|
if has_bar[d] {
|
||||||
let mut frame_state = ui.ctx().frame_state();
|
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
|
||||||
let scroll_delta = frame_state.scroll_delta;
|
|
||||||
|
|
||||||
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
|
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;
|
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
|
||||||
|
@ -677,7 +699,7 @@ impl Prepared {
|
||||||
if scrolling_up || scrolling_down {
|
if scrolling_up || scrolling_down {
|
||||||
state.offset[d] -= scroll_delta[d];
|
state.offset[d] -= scroll_delta[d];
|
||||||
// Clear scroll delta so no parent scroll will use it.
|
// Clear scroll delta so no parent scroll will use it.
|
||||||
frame_state.scroll_delta[d] = 0.0;
|
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
|
||||||
state.scroll_stuck_to_end[d] = false;
|
state.scroll_stuck_to_end[d] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -819,7 +841,7 @@ impl Prepared {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let min_handle_size = ui.spacing().scroll_bar_width;
|
let min_handle_size = ui.spacing().scroll_handle_min_length;
|
||||||
if handle_rect.size()[d] < min_handle_size {
|
if handle_rect.size()[d] < min_handle_size {
|
||||||
handle_rect = Rect::from_center_size(
|
handle_rect = Rect::from_center_size(
|
||||||
handle_rect.center(),
|
handle_rect.center(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ use super::*;
|
||||||
///
|
///
|
||||||
/// You can customize:
|
/// You can customize:
|
||||||
/// * title
|
/// * title
|
||||||
/// * default, minimum, maximum and/or fixed size
|
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
|
||||||
/// * if the window has a scroll area (off by default)
|
/// * 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 the window can be collapsed (minimized) to just the title bar (yes, by default)
|
||||||
/// * if there should be a close button (none by default)
|
/// * if there should be a close button (none by default)
|
||||||
|
@ -30,6 +30,7 @@ pub struct Window<'open> {
|
||||||
resize: Resize,
|
resize: Resize,
|
||||||
scroll: ScrollArea,
|
scroll: ScrollArea,
|
||||||
collapsible: bool,
|
collapsible: bool,
|
||||||
|
default_open: bool,
|
||||||
with_title_bar: bool,
|
with_title_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ impl<'open> Window<'open> {
|
||||||
.default_size([340.0, 420.0]), // Default inner size of a window
|
.default_size([340.0, 420.0]), // Default inner size of a window
|
||||||
scroll: ScrollArea::neither(),
|
scroll: ScrollArea::neither(),
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
default_open: true,
|
||||||
with_title_bar: true,
|
with_title_bar: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +79,18 @@ impl<'open> Window<'open> {
|
||||||
self
|
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))`
|
/// 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.
|
// TODO(emilk): I'm not sure this is a good interface for this.
|
||||||
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
|
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
|
||||||
|
@ -162,6 +176,12 @@ impl<'open> Window<'open> {
|
||||||
self
|
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.
|
/// Set initial size of the window.
|
||||||
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
|
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
|
||||||
self.resize = self.resize.default_size(default_size);
|
self.resize = self.resize.default_size(default_size);
|
||||||
|
@ -275,12 +295,14 @@ impl<'open> Window<'open> {
|
||||||
resize,
|
resize,
|
||||||
scroll,
|
scroll,
|
||||||
collapsible,
|
collapsible,
|
||||||
|
default_open,
|
||||||
with_title_bar,
|
with_title_bar,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
||||||
|
|
||||||
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
|
let is_explicitly_closed = matches!(open, Some(false));
|
||||||
|
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
|
||||||
area.show_open_close_animation(ctx, &frame, is_open);
|
area.show_open_close_animation(ctx, &frame, is_open);
|
||||||
|
|
||||||
if !is_open {
|
if !is_open {
|
||||||
|
@ -291,7 +313,7 @@ impl<'open> Window<'open> {
|
||||||
let area_layer_id = area.layer();
|
let area_layer_id = area.layer();
|
||||||
let resize_id = area_id.with("resize");
|
let resize_id = area_id.with("resize");
|
||||||
let mut collapsing =
|
let mut collapsing =
|
||||||
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
|
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
|
||||||
|
|
||||||
let is_collapsed = with_title_bar && !collapsing.is_open();
|
let is_collapsed = with_title_bar && !collapsing.is_open();
|
||||||
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
|
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
|
||||||
|
@ -318,7 +340,7 @@ impl<'open> Window<'open> {
|
||||||
// Calculate roughly how much larger the window size is compared to the inner rect
|
// Calculate roughly how much larger the window size is compared to the inner rect
|
||||||
let title_bar_height = if with_title_bar {
|
let title_bar_height = if with_title_bar {
|
||||||
let style = ctx.style();
|
let style = ctx.style();
|
||||||
title.font_height(&ctx.fonts(), &style) + title_content_spacing
|
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
@ -404,7 +426,7 @@ impl<'open> Window<'open> {
|
||||||
ctx.style().visuals.widgets.active,
|
ctx.style().visuals.widgets.active,
|
||||||
);
|
);
|
||||||
} else if let Some(hover_interaction) = hover_interaction {
|
} else if let Some(hover_interaction) = hover_interaction {
|
||||||
if ctx.input().pointer.has_pointer() {
|
if ctx.input(|i| i.pointer.has_pointer()) {
|
||||||
paint_frame_interaction(
|
paint_frame_interaction(
|
||||||
&mut area_content_ui,
|
&mut area_content_ui,
|
||||||
outer_rect,
|
outer_rect,
|
||||||
|
@ -416,9 +438,12 @@ impl<'open> Window<'open> {
|
||||||
content_inner
|
content_inner
|
||||||
};
|
};
|
||||||
|
|
||||||
area.state_mut().pos = ctx
|
{
|
||||||
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
let pos = ctx
|
||||||
.min;
|
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
|
||||||
|
.left_top();
|
||||||
|
area.state_mut().set_left_top_pos(pos);
|
||||||
|
}
|
||||||
|
|
||||||
let full_response = area.end(ctx, area_content_ui);
|
let full_response = area.end(ctx, area_content_ui);
|
||||||
|
|
||||||
|
@ -499,13 +524,13 @@ pub(crate) struct WindowInteraction {
|
||||||
impl WindowInteraction {
|
impl WindowInteraction {
|
||||||
pub fn set_cursor(&self, ctx: &Context) {
|
pub fn set_cursor(&self, ctx: &Context) {
|
||||||
if (self.left && self.top) || (self.right && self.bottom) {
|
if (self.left && self.top) || (self.right && self.bottom) {
|
||||||
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
|
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||||
} else if (self.right && self.top) || (self.left && self.bottom) {
|
} else if (self.right && self.top) || (self.left && self.bottom) {
|
||||||
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
|
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
|
||||||
} else if self.left || self.right {
|
} else if self.left || self.right {
|
||||||
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||||
} else if self.bottom || self.top {
|
} else if self.bottom || self.top {
|
||||||
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
|
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,7 +553,7 @@ fn interact(
|
||||||
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds());
|
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"
|
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
|
||||||
area.state_mut().pos = new_rect.min;
|
area.state_mut().set_left_top_pos(new_rect.left_top());
|
||||||
|
|
||||||
if window_interaction.is_resize() {
|
if window_interaction.is_resize() {
|
||||||
if let Some(mut state) = resize::State::load(ctx, resize_id) {
|
if let Some(mut state) = resize::State::load(ctx, resize_id) {
|
||||||
|
@ -537,7 +562,7 @@ fn interact(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.memory().areas.move_to_top(area_layer_id);
|
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
|
||||||
Some(window_interaction)
|
Some(window_interaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,11 +570,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
||||||
window_interaction.set_cursor(ctx);
|
window_interaction.set_cursor(ctx);
|
||||||
|
|
||||||
// Only move/resize windows with primary mouse button:
|
// Only move/resize windows with primary mouse button:
|
||||||
if !ctx.input().pointer.primary_down() {
|
if !ctx.input(|i| i.pointer.primary_down()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pointer_pos = ctx.input().pointer.interact_pos()?;
|
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||||
let mut rect = window_interaction.start_rect; // prevent drift
|
let mut rect = window_interaction.start_rect; // prevent drift
|
||||||
|
|
||||||
if window_interaction.is_resize() {
|
if window_interaction.is_resize() {
|
||||||
|
@ -571,8 +596,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
||||||
// but we want anything interactive in the window (e.g. slider) to steal
|
// 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,
|
// 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.
|
// but instead let other widgets to the steal. HACK.
|
||||||
if !ctx.input().pointer.any_pressed() {
|
if !ctx.input(|i| i.pointer.any_pressed()) {
|
||||||
let press_origin = ctx.input().pointer.press_origin()?;
|
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
|
||||||
let delta = pointer_pos - press_origin;
|
let delta = pointer_pos - press_origin;
|
||||||
rect = rect.translate(delta);
|
rect = rect.translate(delta);
|
||||||
}
|
}
|
||||||
|
@ -590,30 +615,31 @@ fn window_interaction(
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
) -> Option<WindowInteraction> {
|
) -> Option<WindowInteraction> {
|
||||||
{
|
{
|
||||||
let drag_id = ctx.memory().interaction.drag_id;
|
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
|
||||||
|
|
||||||
if drag_id.is_some() && drag_id != Some(id) {
|
if drag_id.is_some() && drag_id != Some(id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut window_interaction = { ctx.memory().window_interaction };
|
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
|
||||||
|
|
||||||
if window_interaction.is_none() {
|
if window_interaction.is_none() {
|
||||||
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
|
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
|
||||||
hover_window_interaction.set_cursor(ctx);
|
hover_window_interaction.set_cursor(ctx);
|
||||||
let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks
|
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
|
||||||
if any_pressed && ctx.input().pointer.primary_down() {
|
ctx.memory_mut(|mem| {
|
||||||
ctx.memory().interaction.drag_id = Some(id);
|
mem.interaction.drag_id = Some(id);
|
||||||
ctx.memory().interaction.drag_is_window = true;
|
mem.interaction.drag_is_window = true;
|
||||||
window_interaction = Some(hover_window_interaction);
|
window_interaction = Some(hover_window_interaction);
|
||||||
ctx.memory().window_interaction = window_interaction;
|
mem.window_interaction = window_interaction;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window_interaction) = window_interaction {
|
if let Some(window_interaction) = window_interaction {
|
||||||
let is_active = ctx.memory().interaction.drag_id == Some(id);
|
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
|
||||||
|
|
||||||
if is_active && window_interaction.area_layer_id == area_layer_id {
|
if is_active && window_interaction.area_layer_id == area_layer_id {
|
||||||
return Some(window_interaction);
|
return Some(window_interaction);
|
||||||
|
@ -629,10 +655,9 @@ fn resize_hover(
|
||||||
area_layer_id: LayerId,
|
area_layer_id: LayerId,
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
) -> Option<WindowInteraction> {
|
) -> Option<WindowInteraction> {
|
||||||
let pointer = ctx.input().pointer.interact_pos()?;
|
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
|
||||||
|
|
||||||
let any_down = ctx.input().pointer.any_down(); // avoid deadlocks
|
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
|
||||||
if any_down && !ctx.input().pointer.any_pressed() {
|
|
||||||
return None; // already dragging (something)
|
return None; // already dragging (something)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +667,7 @@ fn resize_hover(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.memory().interaction.drag_interest {
|
if ctx.memory(|mem| mem.interaction.drag_interest) {
|
||||||
// Another widget will become active if we drag here
|
// Another widget will become active if we drag here
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -804,8 +829,8 @@ fn show_title_bar(
|
||||||
collapsible: bool,
|
collapsible: bool,
|
||||||
) -> TitleBar {
|
) -> TitleBar {
|
||||||
let inner_response = ui.horizontal(|ui| {
|
let inner_response = ui.horizontal(|ui| {
|
||||||
let height = title
|
let height = ui
|
||||||
.font_height(&ui.fonts(), ui.style())
|
.fonts(|fonts| title.font_height(fonts, ui.style()))
|
||||||
.max(ui.spacing().interact_size.y);
|
.max(ui.spacing().interact_size.y);
|
||||||
ui.set_min_height(height);
|
ui.set_min_height(height);
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -314,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
|
||||||
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
|
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
|
||||||
/// as on mac that is how you type special characters,
|
/// as on mac that is how you type special characters,
|
||||||
/// so those key presses are usually not reported to egui.
|
/// so those key presses are usually not reported to egui.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Modifiers {
|
pub struct Modifiers {
|
||||||
/// Either of the alt keys are down (option ⌥ on Mac).
|
/// Either of the alt keys are down (option ⌥ on Mac).
|
||||||
|
@ -777,7 +777,7 @@ impl Key {
|
||||||
///
|
///
|
||||||
/// Can be used with [`crate::InputState::consume_shortcut`]
|
/// Can be used with [`crate::InputState::consume_shortcut`]
|
||||||
/// and [`crate::Context::format_shortcut`].
|
/// and [`crate::Context::format_shortcut`].
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct KeyboardShortcut {
|
pub struct KeyboardShortcut {
|
||||||
pub modifiers: Modifiers,
|
pub modifiers: Modifiers,
|
||||||
pub key: Key,
|
pub key: Key,
|
||||||
|
|
|
@ -70,7 +70,7 @@ pub struct PlatformOutput {
|
||||||
/// ```
|
/// ```
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// if ui.button("📋").clicked() {
|
/// if ui.button("📋").clicked() {
|
||||||
/// ui.output().copied_text = "some_text".to_string();
|
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use crate::*;
|
use crate::{id::IdSet, *};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct TooltipFrameState {
|
pub(crate) struct TooltipFrameState {
|
||||||
|
@ -12,7 +12,7 @@ pub(crate) struct TooltipFrameState {
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct AccessKitFrameState {
|
pub(crate) struct AccessKitFrameState {
|
||||||
pub(crate) nodes: IdMap<Box<accesskit::Node>>,
|
pub(crate) node_builders: IdMap<accesskit::NodeBuilder>,
|
||||||
pub(crate) parent_stack: Vec<Id>,
|
pub(crate) parent_stack: Vec<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,12 @@ pub(crate) struct FrameState {
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||||
|
|
||||||
|
/// Highlight these widgets this next frame. Read from this.
|
||||||
|
pub(crate) highlight_this_frame: IdSet,
|
||||||
|
|
||||||
|
/// Highlight these widgets the next frame. Write to this.
|
||||||
|
pub(crate) highlight_next_frame: IdSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FrameState {
|
impl Default for FrameState {
|
||||||
|
@ -65,6 +71,8 @@ impl Default for FrameState {
|
||||||
scroll_target: [None, None],
|
scroll_target: [None, None],
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit_state: None,
|
accesskit_state: None,
|
||||||
|
highlight_this_frame: Default::default(),
|
||||||
|
highlight_next_frame: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +89,8 @@ impl FrameState {
|
||||||
scroll_target,
|
scroll_target,
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit_state,
|
accesskit_state,
|
||||||
|
highlight_this_frame,
|
||||||
|
highlight_next_frame,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
used_ids.clear();
|
used_ids.clear();
|
||||||
|
@ -90,10 +100,13 @@ impl FrameState {
|
||||||
*tooltip_state = None;
|
*tooltip_state = None;
|
||||||
*scroll_delta = input.scroll_delta;
|
*scroll_delta = input.scroll_delta;
|
||||||
*scroll_target = [None, None];
|
*scroll_target = [None, None];
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
*accesskit_state = None;
|
*accesskit_state = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*highlight_this_frame = std::mem::take(highlight_next_frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How much space is still available after panels has been added.
|
/// How much space is still available after panels has been added.
|
||||||
|
|
|
@ -8,14 +8,14 @@ pub(crate) struct State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data().get_temp(id)
|
ctx.data_mut(|d| d.get_temp(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
// We don't persist Grids, because
|
// We don't persist Grids, because
|
||||||
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
|
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
|
||||||
// B) if the code changes, the grid _should_ change, and not remember old sizes
|
// B) if the code changes, the grid _should_ change, and not remember old sizes
|
||||||
ctx.data().insert_temp(id, self);
|
ctx.data_mut(|d| d.insert_temp(id, self));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_min_col_width(&mut self, col: usize, width: f32) {
|
fn set_min_col_width(&mut self, col: usize, width: f32) {
|
||||||
|
|
|
@ -26,15 +26,15 @@ pub mod kb_shortcuts {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
|
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
|
||||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_RESET) {
|
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
|
||||||
if let Some(native_pixels_per_point) = native_pixels_per_point {
|
if let Some(native_pixels_per_point) = native_pixels_per_point {
|
||||||
ctx.set_pixels_per_point(native_pixels_per_point);
|
ctx.set_pixels_per_point(native_pixels_per_point);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
|
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
|
||||||
zoom_in(ctx);
|
zoom_in(ctx);
|
||||||
}
|
}
|
||||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
|
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
|
||||||
zoom_out(ctx);
|
zoom_out(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,5 +168,8 @@ impl std::hash::BuildHasher for BuilIdHasher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||||
|
pub type IdSet = std::collections::HashSet<Id, BuilIdHasher>;
|
||||||
|
|
||||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||||
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;
|
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;
|
||||||
|
|
|
@ -368,7 +368,7 @@ impl InputState {
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// let mut zoom = 1.0; // no zoom
|
/// let mut zoom = 1.0; // no zoom
|
||||||
/// let mut rotation = 0.0; // no rotation
|
/// let mut rotation = 0.0; // no rotation
|
||||||
/// let multi_touch = ui.input().multi_touch();
|
/// let multi_touch = ui.input(|i| i.multi_touch());
|
||||||
/// if let Some(multi_touch) = multi_touch {
|
/// if let Some(multi_touch) = multi_touch {
|
||||||
/// zoom *= multi_touch.zoom_delta;
|
/// zoom *= multi_touch.zoom_delta;
|
||||||
/// rotation += multi_touch.rotation_delta;
|
/// rotation += multi_touch.rotation_delta;
|
||||||
|
@ -447,9 +447,10 @@ impl InputState {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub(crate) struct Click {
|
pub(crate) struct Click {
|
||||||
pub pos: Pos2,
|
pub pos: Pos2,
|
||||||
pub button: PointerButton,
|
|
||||||
/// 1 or 2 (double-click) or 3 (triple-click)
|
/// 1 or 2 (double-click) or 3 (triple-click)
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
|
|
||||||
/// Allows you to check for e.g. shift-click
|
/// Allows you to check for e.g. shift-click
|
||||||
pub modifiers: Modifiers,
|
pub modifiers: Modifiers,
|
||||||
}
|
}
|
||||||
|
@ -471,7 +472,10 @@ pub(crate) enum PointerEvent {
|
||||||
position: Pos2,
|
position: Pos2,
|
||||||
button: PointerButton,
|
button: PointerButton,
|
||||||
},
|
},
|
||||||
Released(Option<Click>),
|
Released {
|
||||||
|
click: Option<Click>,
|
||||||
|
button: PointerButton,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PointerEvent {
|
impl PointerEvent {
|
||||||
|
@ -480,11 +484,11 @@ impl PointerEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_release(&self) -> bool {
|
pub fn is_release(&self) -> bool {
|
||||||
matches!(self, PointerEvent::Released(_))
|
matches!(self, PointerEvent::Released { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_click(&self) -> bool {
|
pub fn is_click(&self) -> bool {
|
||||||
matches!(self, PointerEvent::Released(Some(_click)))
|
matches!(self, PointerEvent::Released { click: Some(_), .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,7 +643,6 @@ impl PointerState {
|
||||||
|
|
||||||
Some(Click {
|
Some(Click {
|
||||||
pos,
|
pos,
|
||||||
button,
|
|
||||||
count,
|
count,
|
||||||
modifiers,
|
modifiers,
|
||||||
})
|
})
|
||||||
|
@ -647,7 +650,8 @@ impl PointerState {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pointer_events.push(PointerEvent::Released(click));
|
self.pointer_events
|
||||||
|
.push(PointerEvent::Released { click, button });
|
||||||
|
|
||||||
self.press_origin = None;
|
self.press_origin = None;
|
||||||
self.press_start_time = None;
|
self.press_start_time = None;
|
||||||
|
@ -775,11 +779,28 @@ impl PointerState {
|
||||||
self.pointer_events.iter().any(|event| event.is_release())
|
self.pointer_events.iter().any(|event| event.is_release())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Was the button given pressed this frame?
|
||||||
|
pub fn button_pressed(&self, button: PointerButton) -> bool {
|
||||||
|
self.pointer_events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b))
|
||||||
|
}
|
||||||
|
|
||||||
/// Was the button given released this frame?
|
/// Was the button given released this frame?
|
||||||
pub fn button_released(&self, button: PointerButton) -> bool {
|
pub fn button_released(&self, button: PointerButton) -> bool {
|
||||||
self.pointer_events
|
self.pointer_events
|
||||||
.iter()
|
.iter()
|
||||||
.any(|event| matches!(event, &PointerEvent::Released(Some(Click{button: b, ..})) if button == b))
|
.any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Was the primary button pressed this frame?
|
||||||
|
pub fn primary_pressed(&self) -> bool {
|
||||||
|
self.button_pressed(PointerButton::Primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Was the secondary button pressed this frame?
|
||||||
|
pub fn secondary_pressed(&self) -> bool {
|
||||||
|
self.button_pressed(PointerButton::Secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the primary button released this frame?
|
/// Was the primary button released this frame?
|
||||||
|
@ -811,16 +832,28 @@ impl PointerState {
|
||||||
|
|
||||||
/// Was the button given double clicked this frame?
|
/// Was the button given double clicked this frame?
|
||||||
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
|
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
|
||||||
self.pointer_events
|
self.pointer_events.iter().any(|event| {
|
||||||
.iter()
|
matches!(
|
||||||
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
|
&event,
|
||||||
|
PointerEvent::Released {
|
||||||
|
click: Some(click),
|
||||||
|
button: b,
|
||||||
|
} if *b == button && click.is_double()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the button given triple clicked this frame?
|
/// Was the button given triple clicked this frame?
|
||||||
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
|
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
|
||||||
self.pointer_events
|
self.pointer_events.iter().any(|event| {
|
||||||
.iter()
|
matches!(
|
||||||
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
|
&event,
|
||||||
|
PointerEvent::Released {
|
||||||
|
click: Some(click),
|
||||||
|
button: b,
|
||||||
|
} if *b == button && click.is_triple()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the primary button clicked this frame?
|
/// Was the primary button clicked this frame?
|
||||||
|
@ -833,18 +866,6 @@ impl PointerState {
|
||||||
self.button_clicked(PointerButton::Secondary)
|
self.button_clicked(PointerButton::Secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Was this button pressed (`!down -> down`) this frame?
|
|
||||||
// /// This can sometimes return `true` even if `any_down() == false`
|
|
||||||
// /// because a press can be shorted than one frame.
|
|
||||||
// pub fn button_pressed(&self, button: PointerButton) -> bool {
|
|
||||||
// self.pointer_events.iter().any(|event| event.is_press())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Was this button released (`down -> !down`) this frame?
|
|
||||||
// pub fn button_released(&self, button: PointerButton) -> bool {
|
|
||||||
// self.pointer_events.iter().any(|event| event.is_release())
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Is this button currently down?
|
/// Is this button currently down?
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn button_down(&self, button: PointerButton) -> bool {
|
pub fn button_down(&self, button: PointerButton) -> bool {
|
||||||
|
|
|
@ -96,10 +96,14 @@ struct GestureState {
|
||||||
struct DynGestureState {
|
struct DynGestureState {
|
||||||
/// used for proportional zooming
|
/// used for proportional zooming
|
||||||
avg_distance: f32,
|
avg_distance: f32,
|
||||||
|
|
||||||
/// used for non-proportional zooming
|
/// used for non-proportional zooming
|
||||||
avg_abs_distance2: Vec2,
|
avg_abs_distance2: Vec2,
|
||||||
|
|
||||||
avg_pos: Pos2,
|
avg_pos: Pos2,
|
||||||
|
|
||||||
avg_force: f32,
|
avg_force: f32,
|
||||||
|
|
||||||
heading: f32,
|
heading: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||||
let families = ui.fonts().families();
|
let families = ui.fonts(|f| f.families());
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for alternative in families {
|
for alternative in families {
|
||||||
let text = alternative.to_string();
|
let text = alternative.to_string();
|
||||||
|
@ -12,7 +12,7 @@ pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
|
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
|
||||||
let families = ui.fonts().families();
|
let families = ui.fonts(|f| f.families());
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
|
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
|
||||||
for alternative in families {
|
for alternative in families {
|
||||||
|
|
|
@ -39,6 +39,7 @@ impl Order {
|
||||||
Self::Tooltip,
|
Self::Tooltip,
|
||||||
Self::Debug,
|
Self::Debug,
|
||||||
];
|
];
|
||||||
|
pub const TOP: Self = Self::Debug;
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn allow_interaction(&self) -> bool {
|
pub fn allow_interaction(&self) -> bool {
|
||||||
|
|
|
@ -363,11 +363,11 @@ pub use {
|
||||||
input_state::{InputState, MultiTouchInfo, PointerState},
|
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||||
layers::{LayerId, Order},
|
layers::{LayerId, Order},
|
||||||
layout::*,
|
layout::*,
|
||||||
memory::Memory,
|
memory::{Memory, Options},
|
||||||
painter::Painter,
|
painter::Painter,
|
||||||
response::{InnerResponse, Response},
|
response::{InnerResponse, Response},
|
||||||
sense::Sense,
|
sense::Sense,
|
||||||
style::{FontSelection, Style, TextStyle, Visuals},
|
style::{FontSelection, Margin, Style, TextStyle, Visuals},
|
||||||
text::{Galley, TextFormat},
|
text::{Galley, TextFormat},
|
||||||
ui::Ui,
|
ui::Ui,
|
||||||
widget_text::{RichText, WidgetText},
|
widget_text::{RichText, WidgetText},
|
||||||
|
@ -513,18 +513,30 @@ pub mod special_emojis {
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum WidgetType {
|
pub enum WidgetType {
|
||||||
Label, // TODO(emilk): emit Label events
|
Label, // TODO(emilk): emit Label events
|
||||||
|
|
||||||
/// e.g. a hyperlink
|
/// e.g. a hyperlink
|
||||||
Link,
|
Link,
|
||||||
|
|
||||||
TextEdit,
|
TextEdit,
|
||||||
|
|
||||||
Button,
|
Button,
|
||||||
|
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
||||||
RadioButton,
|
RadioButton,
|
||||||
|
|
||||||
SelectableLabel,
|
SelectableLabel,
|
||||||
|
|
||||||
ComboBox,
|
ComboBox,
|
||||||
|
|
||||||
Slider,
|
Slider,
|
||||||
|
|
||||||
DragValue,
|
DragValue,
|
||||||
|
|
||||||
ColorButton,
|
ColorButton,
|
||||||
|
|
||||||
ImageButton,
|
ImageButton,
|
||||||
|
|
||||||
CollapsingHeader,
|
CollapsingHeader,
|
||||||
|
|
||||||
/// If you cannot fit any of the above slots.
|
/// If you cannot fit any of the above slots.
|
||||||
|
@ -559,25 +571,3 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||||
pub fn accesskit_root_id() -> Id {
|
pub fn accesskit_root_id() -> Id {
|
||||||
Id::new("accesskit_root")
|
Id::new("accesskit_root")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a tree update that the egui integration should provide to the
|
|
||||||
/// AccessKit adapter if it cannot immediately run the egui application
|
|
||||||
/// to get a full tree update after running [`Context::enable_accesskit`].
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
|
|
||||||
use accesskit::{Node, Role, Tree, TreeUpdate};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
let root_id = accesskit_root_id().accesskit_id();
|
|
||||||
TreeUpdate {
|
|
||||||
nodes: vec![(
|
|
||||||
root_id,
|
|
||||||
Arc::new(Node {
|
|
||||||
role: Role::Window,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
)],
|
|
||||||
tree: Some(Tree::new(root_id)),
|
|
||||||
focus: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -52,9 +52,10 @@ pub struct Memory {
|
||||||
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
||||||
///
|
///
|
||||||
/// # let mut ctx = egui::Context::default();
|
/// # let mut ctx = egui::Context::default();
|
||||||
/// let mut memory = ctx.memory();
|
/// ctx.memory_mut(|mem| {
|
||||||
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
|
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
|
||||||
/// assert_eq!(cache.get("hello"), 5);
|
/// assert_eq!(cache.get("hello"), 5);
|
||||||
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
pub caches: crate::util::cache::CacheStorage,
|
pub caches: crate::util::cache::CacheStorage,
|
||||||
|
@ -102,9 +103,15 @@ pub struct Options {
|
||||||
/// Controls the tessellator.
|
/// Controls the tessellator.
|
||||||
pub tessellation_options: epaint::TessellationOptions,
|
pub tessellation_options: epaint::TessellationOptions,
|
||||||
|
|
||||||
/// This does not at all change the behavior of egui,
|
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||||
/// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
///
|
||||||
|
/// The only change to egui is that labels can be focused by pressing tab.
|
||||||
|
///
|
||||||
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
|
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
|
||||||
|
///
|
||||||
|
/// `eframe` supports it only on web, using the `web_screen_reader` feature flag,
|
||||||
|
/// but you should consider using [AccessKit](https://github.com/AccessKit/accesskit) instead,
|
||||||
|
/// which `eframe` supports.
|
||||||
pub screen_reader: bool,
|
pub screen_reader: bool,
|
||||||
|
|
||||||
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
|
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
|
||||||
|
@ -405,7 +412,7 @@ impl Memory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
|
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
|
||||||
pub fn has_lock_focus(&mut self, id: Id) -> bool {
|
pub fn has_lock_focus(&self, id: Id) -> bool {
|
||||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||||
self.interaction.focus.is_focus_locked
|
self.interaction.focus.is_focus_locked
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -31,11 +31,11 @@ pub(crate) struct BarState {
|
||||||
|
|
||||||
impl BarState {
|
impl BarState {
|
||||||
fn load(ctx: &Context, bar_id: Id) -> Self {
|
fn load(ctx: &Context, bar_id: Id) -> Self {
|
||||||
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
|
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(self, ctx: &Context, bar_id: Id) {
|
fn store(self, ctx: &Context, bar_id: Id) {
|
||||||
ctx.data().insert_temp(bar_id, self);
|
ctx.data_mut(|d| d.insert_temp(bar_id, self));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a menu at pointer if primary-clicked response.
|
/// Show a menu at pointer if primary-clicked response.
|
||||||
|
@ -68,7 +68,7 @@ fn set_menu_style(style: &mut Style) {
|
||||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||||
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||||
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||||||
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
|
||||||
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +100,20 @@ pub fn menu_button<R>(
|
||||||
stationary_menu_impl(ui, title, Box::new(add_contents))
|
stationary_menu_impl(ui, title, Box::new(add_contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
|
||||||
|
///
|
||||||
|
/// Responds to primary clicks.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the menu is not open.
|
||||||
|
pub fn menu_image_button<R>(
|
||||||
|
ui: &mut Ui,
|
||||||
|
texture_id: TextureId,
|
||||||
|
image_size: impl Into<Vec2>,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> InnerResponse<Option<R>> {
|
||||||
|
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct a nested sub menu in another menu.
|
/// Construct a nested sub menu in another menu.
|
||||||
///
|
///
|
||||||
/// Opens on hover.
|
/// Opens on hover.
|
||||||
|
@ -129,9 +143,10 @@ pub(crate) fn menu_ui<'c, R>(
|
||||||
|
|
||||||
let area = Area::new(menu_id)
|
let area = Area::new(menu_id)
|
||||||
.order(Order::Foreground)
|
.order(Order::Foreground)
|
||||||
|
.constrain(true)
|
||||||
.fixed_pos(pos)
|
.fixed_pos(pos)
|
||||||
.interactable(true)
|
.interactable(true)
|
||||||
.drag_bounds(Rect::EVERYTHING);
|
.drag_bounds(ctx.screen_rect());
|
||||||
let inner_response = area.show(ctx, |ui| {
|
let inner_response = area.show(ctx, |ui| {
|
||||||
set_menu_style(ui.style_mut());
|
set_menu_style(ui.style_mut());
|
||||||
|
|
||||||
|
@ -166,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
|
||||||
let mut button = Button::new(title);
|
let mut button = Button::new(title);
|
||||||
|
|
||||||
if bar_state.open_menu.is_menu_open(menu_id) {
|
if bar_state.open_menu.is_menu_open(menu_id) {
|
||||||
button = button.fill(ui.visuals().widgets.open.bg_fill);
|
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
|
||||||
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +192,25 @@ fn stationary_menu_impl<'c, R>(
|
||||||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a top level menu with an image button.
|
||||||
|
///
|
||||||
|
/// Responds to primary clicks.
|
||||||
|
fn stationary_menu_image_impl<'c, R>(
|
||||||
|
ui: &mut Ui,
|
||||||
|
texture_id: TextureId,
|
||||||
|
image_size: impl Into<Vec2>,
|
||||||
|
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||||
|
) -> InnerResponse<Option<R>> {
|
||||||
|
let bar_id = ui.id();
|
||||||
|
|
||||||
|
let mut bar_state = BarState::load(ui.ctx(), bar_id);
|
||||||
|
let button_response = ui.add(ImageButton::new(texture_id, image_size));
|
||||||
|
let inner = bar_state.bar_menu(&button_response, add_contents);
|
||||||
|
|
||||||
|
bar_state.store(ui.ctx(), bar_id);
|
||||||
|
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||||
pub(crate) fn context_menu(
|
pub(crate) fn context_menu(
|
||||||
response: &Response,
|
response: &Response,
|
||||||
|
@ -277,10 +311,9 @@ impl MenuRoot {
|
||||||
root: &mut MenuRootManager,
|
root: &mut MenuRootManager,
|
||||||
id: Id,
|
id: Id,
|
||||||
) -> MenuResponse {
|
) -> MenuResponse {
|
||||||
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
|
if (response.clicked() && root.is_menu_open(id))
|
||||||
let input = response.ctx.input();
|
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
|
||||||
|
{
|
||||||
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
|
|
||||||
// menu open and button clicked or esc pressed
|
// menu open and button clicked or esc pressed
|
||||||
return MenuResponse::Close;
|
return MenuResponse::Close;
|
||||||
} else if (response.clicked() && !root.is_menu_open(id))
|
} else if (response.clicked() && !root.is_menu_open(id))
|
||||||
|
@ -288,12 +321,10 @@ impl MenuRoot {
|
||||||
{
|
{
|
||||||
// menu not open and button clicked
|
// menu not open and button clicked
|
||||||
// or button hovered while other menu is open
|
// or button hovered while other menu is open
|
||||||
drop(input);
|
|
||||||
|
|
||||||
let mut pos = response.rect.left_bottom();
|
let mut pos = response.rect.left_bottom();
|
||||||
if let Some(root) = root.inner.as_mut() {
|
if let Some(root) = root.inner.as_mut() {
|
||||||
let menu_rect = root.menu_state.read().rect;
|
let menu_rect = root.menu_state.read().rect;
|
||||||
let screen_rect = response.ctx.input().screen_rect;
|
let screen_rect = response.ctx.input(|i| i.screen_rect);
|
||||||
|
|
||||||
if pos.y + menu_rect.height() > screen_rect.max.y {
|
if pos.y + menu_rect.height() > screen_rect.max.y {
|
||||||
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
|
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
|
||||||
|
@ -305,8 +336,11 @@ impl MenuRoot {
|
||||||
}
|
}
|
||||||
|
|
||||||
return MenuResponse::Create(pos, id);
|
return MenuResponse::Create(pos, id);
|
||||||
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
|
} else if response
|
||||||
if let Some(pos) = input.pointer.interact_pos() {
|
.ctx
|
||||||
|
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
|
||||||
|
{
|
||||||
|
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
|
||||||
if let Some(root) = root.inner.as_mut() {
|
if let Some(root) = root.inner.as_mut() {
|
||||||
if root.id == id {
|
if root.id == id {
|
||||||
// pressed somewhere while this menu is open
|
// pressed somewhere while this menu is open
|
||||||
|
@ -329,26 +363,28 @@ impl MenuRoot {
|
||||||
id: Id,
|
id: Id,
|
||||||
) -> MenuResponse {
|
) -> MenuResponse {
|
||||||
let response = response.interact(Sense::click());
|
let response = response.interact(Sense::click());
|
||||||
let pointer = &response.ctx.input().pointer;
|
response.ctx.input(|input| {
|
||||||
if pointer.any_pressed() {
|
let pointer = &input.pointer;
|
||||||
if let Some(pos) = pointer.interact_pos() {
|
if pointer.any_pressed() {
|
||||||
let mut destroy = false;
|
if let Some(pos) = pointer.interact_pos() {
|
||||||
let mut in_old_menu = false;
|
let mut destroy = false;
|
||||||
if let Some(root) = root {
|
let mut in_old_menu = false;
|
||||||
let menu_state = root.menu_state.read();
|
if let Some(root) = root {
|
||||||
in_old_menu = menu_state.area_contains(pos);
|
let menu_state = root.menu_state.read();
|
||||||
destroy = root.id == response.id;
|
in_old_menu = menu_state.area_contains(pos);
|
||||||
}
|
destroy = root.id == response.id;
|
||||||
if !in_old_menu {
|
}
|
||||||
if response.hovered() && pointer.secondary_down() {
|
if !in_old_menu {
|
||||||
return MenuResponse::Create(pos, id);
|
if response.hovered() && pointer.secondary_down() {
|
||||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
return MenuResponse::Create(pos, id);
|
||||||
return MenuResponse::Close;
|
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||||
|
return MenuResponse::Close;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
MenuResponse::Stay
|
||||||
MenuResponse::Stay
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
||||||
|
@ -410,7 +446,7 @@ impl SubMenuButton {
|
||||||
sub_id: Id,
|
sub_id: Id,
|
||||||
) -> &'a WidgetVisuals {
|
) -> &'a WidgetVisuals {
|
||||||
if menu_state.is_open(sub_id) {
|
if menu_state.is_open(sub_id) {
|
||||||
&ui.style().visuals.widgets.hovered
|
&ui.style().visuals.widgets.open
|
||||||
} else {
|
} else {
|
||||||
ui.style().interact(response)
|
ui.style().interact(response)
|
||||||
}
|
}
|
||||||
|
@ -439,7 +475,8 @@ impl SubMenuButton {
|
||||||
text_galley.size().x + icon_galley.size().x,
|
text_galley.size().x + icon_galley.size().x,
|
||||||
text_galley.size().y.max(icon_galley.size().y),
|
text_galley.size().y.max(icon_galley.size().y),
|
||||||
);
|
);
|
||||||
let desired_size = text_and_icon_size + 2.0 * button_padding;
|
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||||
|
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||||
response.widget_info(|| {
|
response.widget_info(|| {
|
||||||
|
@ -459,7 +496,7 @@ impl SubMenuButton {
|
||||||
ui.painter().rect_filled(
|
ui.painter().rect_filled(
|
||||||
rect.expand(visuals.expansion),
|
rect.expand(visuals.expansion),
|
||||||
visuals.rounding,
|
visuals.rounding,
|
||||||
visuals.bg_fill,
|
visuals.weak_bg_fill,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,15 +608,15 @@ impl MenuState {
|
||||||
|
|
||||||
/// Sense button interaction opening and closing submenu.
|
/// Sense button interaction opening and closing submenu.
|
||||||
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
|
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
|
||||||
let pointer = &ui.input().pointer.clone();
|
let pointer = ui.input(|i| i.pointer.clone());
|
||||||
let open = self.is_open(sub_id);
|
let open = self.is_open(sub_id);
|
||||||
if self.moving_towards_current_submenu(pointer) {
|
if self.moving_towards_current_submenu(&pointer) {
|
||||||
// ensure to repaint once even when pointer is not moving
|
// ensure to repaint once even when pointer is not moving
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
} else if !open && button.hovered() {
|
} else if !open && button.hovered() {
|
||||||
let pos = button.rect.right_top();
|
let pos = button.rect.right_top();
|
||||||
self.open_submenu(sub_id, pos);
|
self.open_submenu(sub_id, pos);
|
||||||
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
|
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
|
||||||
self.close_submenu();
|
self.close_submenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ use crate::{
|
||||||
Color32, Context, FontId,
|
Color32, Context, FontId,
|
||||||
};
|
};
|
||||||
use epaint::{
|
use epaint::{
|
||||||
mutex::{RwLockReadGuard, RwLockWriteGuard},
|
|
||||||
text::{Fonts, Galley},
|
text::{Fonts, Galley},
|
||||||
CircleShape, RectShape, Rounding, Shape, Stroke,
|
CircleShape, RectShape, Rounding, Shape, Stroke,
|
||||||
};
|
};
|
||||||
|
@ -105,10 +104,12 @@ impl Painter {
|
||||||
&self.ctx
|
&self.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Available fonts.
|
/// Read-only access to the shared [`Fonts`].
|
||||||
|
///
|
||||||
|
/// See [`Context`] documentation for how locks work.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||||
self.ctx.fonts()
|
self.ctx.fonts(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where we paint
|
/// Where we paint
|
||||||
|
@ -152,8 +153,9 @@ impl Painter {
|
||||||
|
|
||||||
/// ## Low level
|
/// ## Low level
|
||||||
impl Painter {
|
impl Painter {
|
||||||
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
|
#[inline]
|
||||||
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
|
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
|
||||||
|
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transform_shape(&self, shape: &mut Shape) {
|
fn transform_shape(&self, shape: &mut Shape) {
|
||||||
|
@ -167,11 +169,11 @@ impl Painter {
|
||||||
/// NOTE: all coordinates are screen coordinates!
|
/// NOTE: all coordinates are screen coordinates!
|
||||||
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
|
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
|
||||||
if self.fade_to_color == Some(Color32::TRANSPARENT) {
|
if self.fade_to_color == Some(Color32::TRANSPARENT) {
|
||||||
self.paint_list().add(self.clip_rect, Shape::Noop)
|
self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
|
||||||
} else {
|
} else {
|
||||||
let mut shape = shape.into();
|
let mut shape = shape.into();
|
||||||
self.transform_shape(&mut shape);
|
self.transform_shape(&mut shape);
|
||||||
self.paint_list().add(self.clip_rect, shape)
|
self.paint_list(|l| l.add(self.clip_rect, shape))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,9 +189,9 @@ impl Painter {
|
||||||
self.transform_shape(&mut shape);
|
self.transform_shape(&mut shape);
|
||||||
shape
|
shape
|
||||||
});
|
});
|
||||||
self.paint_list().extend(self.clip_rect, shapes);
|
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||||
} else {
|
} else {
|
||||||
self.paint_list().extend(self.clip_rect, shapes);
|
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +202,7 @@ impl Painter {
|
||||||
}
|
}
|
||||||
let mut shape = shape.into();
|
let mut shape = shape.into();
|
||||||
self.transform_shape(&mut shape);
|
self.transform_shape(&mut shape);
|
||||||
self.paint_list().set(idx, self.clip_rect, shape);
|
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,7 +407,7 @@ impl Painter {
|
||||||
color: crate::Color32,
|
color: crate::Color32,
|
||||||
wrap_width: f32,
|
wrap_width: f32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.fonts().layout(text, font_id, color, wrap_width)
|
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Will line break at `\n`.
|
/// Will line break at `\n`.
|
||||||
|
@ -418,7 +420,7 @@ impl Painter {
|
||||||
font_id: FontId,
|
font_id: FontId,
|
||||||
color: crate::Color32,
|
color: crate::Color32,
|
||||||
) -> Arc<Galley> {
|
) -> Arc<Galley> {
|
||||||
self.fonts().layout(text, font_id, color, f32::INFINITY)
|
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint text that has already been layed out in a [`Galley`].
|
/// Paint text that has already been layed out in a [`Galley`].
|
||||||
|
|
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
///
|
///
|
||||||
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
||||||
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
||||||
|
// TODO(emilk): we should be using bit sets instead of so many bools
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
// CONTEXT:
|
// CONTEXT:
|
||||||
|
@ -42,6 +43,10 @@ pub struct Response {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub hovered: bool,
|
pub hovered: bool,
|
||||||
|
|
||||||
|
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub highlighted: bool,
|
||||||
|
|
||||||
/// The pointer clicked this thing this frame.
|
/// The pointer clicked this thing this frame.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub clicked: [bool; NUM_POINTER_BUTTONS],
|
pub clicked: [bool; NUM_POINTER_BUTTONS],
|
||||||
|
@ -52,7 +57,7 @@ pub struct Response {
|
||||||
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
|
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||||
|
|
||||||
/// The thing was triple-clicked.
|
/// The thing was triple-clicked.
|
||||||
pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||||
|
|
||||||
/// The widgets is being dragged
|
/// The widgets is being dragged
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -90,6 +95,7 @@ impl std::fmt::Debug for Response {
|
||||||
sense,
|
sense,
|
||||||
enabled,
|
enabled,
|
||||||
hovered,
|
hovered,
|
||||||
|
highlighted,
|
||||||
clicked,
|
clicked,
|
||||||
double_clicked,
|
double_clicked,
|
||||||
triple_clicked,
|
triple_clicked,
|
||||||
|
@ -106,6 +112,7 @@ impl std::fmt::Debug for Response {
|
||||||
.field("sense", sense)
|
.field("sense", sense)
|
||||||
.field("enabled", enabled)
|
.field("enabled", enabled)
|
||||||
.field("hovered", hovered)
|
.field("hovered", hovered)
|
||||||
|
.field("highlighted", highlighted)
|
||||||
.field("clicked", clicked)
|
.field("clicked", clicked)
|
||||||
.field("double_clicked", double_clicked)
|
.field("double_clicked", double_clicked)
|
||||||
.field("triple_clicked", triple_clicked)
|
.field("triple_clicked", triple_clicked)
|
||||||
|
@ -173,23 +180,25 @@ impl Response {
|
||||||
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
||||||
// even if we aren't clickable (or even enabled).
|
// even if we aren't clickable (or even enabled).
|
||||||
// This is important for windows and such that should close then the user clicks elsewhere.
|
// This is important for windows and such that should close then the user clicks elsewhere.
|
||||||
let pointer = &self.ctx.input().pointer;
|
self.ctx.input(|i| {
|
||||||
|
let pointer = &i.pointer;
|
||||||
|
|
||||||
if pointer.any_click() {
|
if pointer.any_click() {
|
||||||
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
||||||
// self.rect. See Context::interact.
|
// self.rect. See Context::interact.
|
||||||
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
||||||
// hence the extra complexity here.
|
// hence the extra complexity here.
|
||||||
if self.hovered() {
|
if self.hovered() {
|
||||||
false
|
false
|
||||||
} else if let Some(pos) = pointer.interact_pos() {
|
} else if let Some(pos) = pointer.interact_pos() {
|
||||||
!self.rect.contains(pos)
|
!self.rect.contains(pos)
|
||||||
|
} else {
|
||||||
|
false // clicked without a pointer, weird
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
false // clicked without a pointer, weird
|
false
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Was the widget enabled?
|
/// Was the widget enabled?
|
||||||
|
@ -211,20 +220,24 @@ impl Response {
|
||||||
self.hovered
|
self.hovered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn highlighted(&self) -> bool {
|
||||||
|
self.highlighted
|
||||||
|
}
|
||||||
|
|
||||||
/// This widget has the keyboard focus (i.e. is receiving key presses).
|
/// This widget has the keyboard focus (i.e. is receiving key presses).
|
||||||
///
|
///
|
||||||
/// This function only returns true if the UI as a whole (e.g. window)
|
/// This function only returns true if the UI as a whole (e.g. window)
|
||||||
/// also has the keyboard focus. That makes this function suitable
|
/// also has the keyboard focus. That makes this function suitable
|
||||||
/// for style choices, e.g. a thicker border around focused widgets.
|
/// for style choices, e.g. a thicker border around focused widgets.
|
||||||
pub fn has_focus(&self) -> bool {
|
pub fn has_focus(&self) -> bool {
|
||||||
// Access input and memory in separate statements to prevent deadlock.
|
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
|
||||||
let has_global_focus = self.ctx.input().raw.has_focus;
|
|
||||||
has_global_focus && self.ctx.memory().has_focus(self.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if this widget has keyboard focus this frame, but didn't last frame.
|
/// True if this widget has keyboard focus this frame, but didn't last frame.
|
||||||
pub fn gained_focus(&self) -> bool {
|
pub fn gained_focus(&self) -> bool {
|
||||||
self.ctx.memory().gained_focus(self.id)
|
self.ctx.memory(|mem| mem.gained_focus(self.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The widget had keyboard focus and lost it,
|
/// The widget had keyboard focus and lost it,
|
||||||
|
@ -236,29 +249,29 @@ impl Response {
|
||||||
/// # let mut my_text = String::new();
|
/// # let mut my_text = String::new();
|
||||||
/// # fn do_request(_: &str) {}
|
/// # fn do_request(_: &str) {}
|
||||||
/// let response = ui.text_edit_singleline(&mut my_text);
|
/// let response = ui.text_edit_singleline(&mut my_text);
|
||||||
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
/// do_request(&my_text);
|
/// do_request(&my_text);
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn lost_focus(&self) -> bool {
|
pub fn lost_focus(&self) -> bool {
|
||||||
self.ctx.memory().lost_focus(self.id)
|
self.ctx.memory(|mem| mem.lost_focus(self.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request that this widget get keyboard focus.
|
/// Request that this widget get keyboard focus.
|
||||||
pub fn request_focus(&self) {
|
pub fn request_focus(&self) {
|
||||||
self.ctx.memory().request_focus(self.id);
|
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surrender keyboard focus for this widget.
|
/// Surrender keyboard focus for this widget.
|
||||||
pub fn surrender_focus(&self) {
|
pub fn surrender_focus(&self) {
|
||||||
self.ctx.memory().surrender_focus(self.id);
|
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The widgets is being dragged.
|
/// The widgets is being dragged.
|
||||||
///
|
///
|
||||||
/// To find out which button(s), query [`crate::PointerState::button_down`]
|
/// To find out which button(s), query [`crate::PointerState::button_down`]
|
||||||
/// (`ui.input().pointer.button_down(…)`).
|
/// (`ui.input(|i| i.pointer.button_down(…))`).
|
||||||
///
|
///
|
||||||
/// Note that the widget must be sensing drags with [`Sense::drag`].
|
/// Note that the widget must be sensing drags with [`Sense::drag`].
|
||||||
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
||||||
|
@ -270,12 +283,17 @@ impl Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
||||||
self.dragged() && self.ctx.input().pointer.button_down(button)
|
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Did a drag on this widgets begin this frame?
|
/// Did a drag on this widgets begin this frame?
|
||||||
pub fn drag_started(&self) -> bool {
|
pub fn drag_started(&self) -> bool {
|
||||||
self.dragged && self.ctx.input().pointer.any_pressed()
|
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Did a drag on this widgets by the button begin this frame?
|
||||||
|
pub fn drag_started_by(&self, button: PointerButton) -> bool {
|
||||||
|
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The widget was being dragged, but now it has been released.
|
/// The widget was being dragged, but now it has been released.
|
||||||
|
@ -283,10 +301,15 @@ impl Response {
|
||||||
self.drag_released
|
self.drag_released
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The widget was being dragged by the button, but now it has been released.
|
||||||
|
pub fn drag_released_by(&self, button: PointerButton) -> bool {
|
||||||
|
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
|
||||||
|
}
|
||||||
|
|
||||||
/// If dragged, how many points were we dragged and in what direction?
|
/// If dragged, how many points were we dragged and in what direction?
|
||||||
pub fn drag_delta(&self) -> Vec2 {
|
pub fn drag_delta(&self) -> Vec2 {
|
||||||
if self.dragged() {
|
if self.dragged() {
|
||||||
self.ctx.input().pointer.delta()
|
self.ctx.input(|i| i.pointer.delta())
|
||||||
} else {
|
} else {
|
||||||
Vec2::ZERO
|
Vec2::ZERO
|
||||||
}
|
}
|
||||||
|
@ -302,7 +325,7 @@ impl Response {
|
||||||
/// None if the pointer is outside the response area.
|
/// None if the pointer is outside the response area.
|
||||||
pub fn hover_pos(&self) -> Option<Pos2> {
|
pub fn hover_pos(&self) -> Option<Pos2> {
|
||||||
if self.hovered() {
|
if self.hovered() {
|
||||||
self.ctx.input().pointer.hover_pos()
|
self.ctx.input(|i| i.pointer.hover_pos())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -392,11 +415,11 @@ impl Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_show_hover_ui(&self) -> bool {
|
fn should_show_hover_ui(&self) -> bool {
|
||||||
if self.ctx.memory().everything_is_visible() {
|
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
|
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,8 +427,7 @@ impl Response {
|
||||||
// We only show the tooltip when the mouse pointer is still,
|
// We only show the tooltip when the mouse pointer is still,
|
||||||
// but once shown we keep showing it until the mouse leaves the parent.
|
// but once shown we keep showing it until the mouse leaves the parent.
|
||||||
|
|
||||||
let is_pointer_still = self.ctx.input().pointer.is_still();
|
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
|
||||||
if !is_pointer_still && !self.is_tooltip_open() {
|
|
||||||
// wait for mouse to stop
|
// wait for mouse to stop
|
||||||
self.ctx.request_repaint();
|
self.ctx.request_repaint();
|
||||||
return false;
|
return false;
|
||||||
|
@ -414,8 +436,9 @@ impl Response {
|
||||||
|
|
||||||
// We don't want tooltips of things while we are dragging them,
|
// We don't want tooltips of things while we are dragging them,
|
||||||
// but we do want tooltips while holding down on an item on a touch screen.
|
// but we do want tooltips while holding down on an item on a touch screen.
|
||||||
if self.ctx.input().pointer.any_down()
|
if self
|
||||||
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
|
.ctx
|
||||||
|
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -444,6 +467,17 @@ impl Response {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
|
||||||
|
///
|
||||||
|
/// The highlight takes on frame to take effect if you call this after the widget has been fully rendered.
|
||||||
|
///
|
||||||
|
/// See also [`Context::highlight_widget`].
|
||||||
|
pub fn highlight(mut self) -> Self {
|
||||||
|
self.ctx.highlight_widget(self.id);
|
||||||
|
self.highlighted = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Show this text when hovering if the widget is disabled.
|
/// Show this text when hovering if the widget is disabled.
|
||||||
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
|
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
|
||||||
self.on_disabled_hover_ui(|ui| {
|
self.on_disabled_hover_ui(|ui| {
|
||||||
|
@ -454,7 +488,7 @@ impl Response {
|
||||||
/// When hovered, use this icon for the mouse cursor.
|
/// When hovered, use this icon for the mouse cursor.
|
||||||
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
|
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
|
||||||
if self.hovered() {
|
if self.hovered() {
|
||||||
self.ctx.output().cursor_icon = cursor;
|
self.ctx.set_cursor_icon(cursor);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -462,7 +496,7 @@ impl Response {
|
||||||
/// When hovered or dragged, use this icon for the mouse cursor.
|
/// When hovered or dragged, use this icon for the mouse cursor.
|
||||||
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
|
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
|
||||||
if self.hovered() || self.dragged() {
|
if self.hovered() || self.dragged() {
|
||||||
self.ctx.output().cursor_icon = cursor;
|
self.ctx.set_cursor_icon(cursor);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -511,8 +545,10 @@ impl Response {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scroll_to_me(&self, align: Option<Align>) {
|
pub fn scroll_to_me(&self, align: Option<Align>) {
|
||||||
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
|
self.ctx.frame_state_mut(|state| {
|
||||||
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
|
state.scroll_target[0] = Some((self.rect.x_range(), align));
|
||||||
|
state.scroll_target[1] = Some((self.rect.y_range(), align));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For accessibility.
|
/// For accessibility.
|
||||||
|
@ -537,47 +573,47 @@ impl Response {
|
||||||
self.output_event(event);
|
self.output_event(event);
|
||||||
} else {
|
} else {
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||||
self.fill_accesskit_node_from_widget_info(&mut node, make_info());
|
self.fill_accesskit_node_from_widget_info(builder, make_info());
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||||
self.fill_accesskit_node_from_widget_info(&mut node, event.widget_info().clone());
|
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
|
||||||
}
|
});
|
||||||
self.ctx.output().events.push(event);
|
self.ctx.output_mut(|o| o.events.push(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
|
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) {
|
||||||
node.bounds = Some(accesskit::kurbo::Rect {
|
builder.set_bounds(accesskit::Rect {
|
||||||
x0: self.rect.min.x.into(),
|
x0: self.rect.min.x.into(),
|
||||||
y0: self.rect.min.y.into(),
|
y0: self.rect.min.y.into(),
|
||||||
x1: self.rect.max.x.into(),
|
x1: self.rect.max.x.into(),
|
||||||
y1: self.rect.max.y.into(),
|
y1: self.rect.max.y.into(),
|
||||||
});
|
});
|
||||||
if self.sense.focusable {
|
if self.sense.focusable {
|
||||||
node.focusable = true;
|
builder.add_action(accesskit::Action::Focus);
|
||||||
}
|
}
|
||||||
if self.sense.click && node.default_action_verb.is_none() {
|
if self.sense.click && builder.default_action_verb().is_none() {
|
||||||
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
|
builder.set_default_action_verb(accesskit::DefaultActionVerb::Click);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
fn fill_accesskit_node_from_widget_info(
|
fn fill_accesskit_node_from_widget_info(
|
||||||
&self,
|
&self,
|
||||||
node: &mut accesskit::Node,
|
builder: &mut accesskit::NodeBuilder,
|
||||||
info: crate::WidgetInfo,
|
info: crate::WidgetInfo,
|
||||||
) {
|
) {
|
||||||
use crate::WidgetType;
|
use crate::WidgetType;
|
||||||
use accesskit::{CheckedState, Role};
|
use accesskit::{CheckedState, Role};
|
||||||
|
|
||||||
self.fill_accesskit_node_common(node);
|
self.fill_accesskit_node_common(builder);
|
||||||
node.role = match info.typ {
|
builder.set_role(match info.typ {
|
||||||
WidgetType::Label => Role::StaticText,
|
WidgetType::Label => Role::StaticText,
|
||||||
WidgetType::Link => Role::Link,
|
WidgetType::Link => Role::Link,
|
||||||
WidgetType::TextEdit => Role::TextField,
|
WidgetType::TextEdit => Role::TextField,
|
||||||
|
@ -592,18 +628,18 @@ impl Response {
|
||||||
WidgetType::DragValue => Role::SpinButton,
|
WidgetType::DragValue => Role::SpinButton,
|
||||||
WidgetType::ColorButton => Role::ColorWell,
|
WidgetType::ColorButton => Role::ColorWell,
|
||||||
WidgetType::Other => Role::Unknown,
|
WidgetType::Other => Role::Unknown,
|
||||||
};
|
});
|
||||||
if let Some(label) = info.label {
|
if let Some(label) = info.label {
|
||||||
node.name = Some(label.into());
|
builder.set_name(label);
|
||||||
}
|
}
|
||||||
if let Some(value) = info.current_text_value {
|
if let Some(value) = info.current_text_value {
|
||||||
node.value = Some(value.into());
|
builder.set_value(value);
|
||||||
}
|
}
|
||||||
if let Some(value) = info.value {
|
if let Some(value) = info.value {
|
||||||
node.numeric_value = Some(value);
|
builder.set_numeric_value(value);
|
||||||
}
|
}
|
||||||
if let Some(selected) = info.selected {
|
if let Some(selected) = info.selected {
|
||||||
node.checked_state = Some(if selected {
|
builder.set_checked_state(if selected {
|
||||||
CheckedState::True
|
CheckedState::True
|
||||||
} else {
|
} else {
|
||||||
CheckedState::False
|
CheckedState::False
|
||||||
|
@ -626,9 +662,9 @@ impl Response {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn labelled_by(self, id: Id) -> Self {
|
pub fn labelled_by(self, id: Id) -> Self {
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = self.ctx.accesskit_node(self.id) {
|
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||||
node.labelled_by.push(id.accesskit_id());
|
builder.push_labelled_by(id.accesskit_id());
|
||||||
}
|
});
|
||||||
#[cfg(not(feature = "accesskit"))]
|
#[cfg(not(feature = "accesskit"))]
|
||||||
{
|
{
|
||||||
let _ = id;
|
let _ = id;
|
||||||
|
@ -677,6 +713,7 @@ impl Response {
|
||||||
sense: self.sense.union(other.sense),
|
sense: self.sense.union(other.sense),
|
||||||
enabled: self.enabled || other.enabled,
|
enabled: self.enabled || other.enabled,
|
||||||
hovered: self.hovered || other.hovered,
|
hovered: self.hovered || other.hovered,
|
||||||
|
highlighted: self.highlighted || other.highlighted,
|
||||||
clicked: [
|
clicked: [
|
||||||
self.clicked[0] || other.clicked[0],
|
self.clicked[0] || other.clicked[0],
|
||||||
self.clicked[1] || other.clicked[1],
|
self.clicked[1] || other.clicked[1],
|
||||||
|
@ -708,6 +745,13 @@ impl Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Returns a response with a modified [`Self::rect`].
|
||||||
|
pub fn with_new_rect(self, rect: Rect) -> Self {
|
||||||
|
Self { rect, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// To summarize the response from many widgets you can use this pattern:
|
/// To summarize the response from many widgets you can use this pattern:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -174,6 +174,9 @@ pub struct Style {
|
||||||
/// ```
|
/// ```
|
||||||
pub text_styles: BTreeMap<TextStyle, FontId>,
|
pub text_styles: BTreeMap<TextStyle, FontId>,
|
||||||
|
|
||||||
|
/// The style to use for [`DragValue`] text.
|
||||||
|
pub drag_value_text_style: TextStyle,
|
||||||
|
|
||||||
/// If set, labels buttons wtc will use this to determine whether or not
|
/// If set, labels buttons wtc will use this to determine whether or not
|
||||||
/// to wrap the text at the right edge of the [`Ui`] they are in.
|
/// to wrap the text at the right edge of the [`Ui`] they are in.
|
||||||
/// By default this is `None`.
|
/// By default this is `None`.
|
||||||
|
@ -216,6 +219,7 @@ impl Style {
|
||||||
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
|
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
|
||||||
let mut visuals = *self.visuals.widgets.style(response);
|
let mut visuals = *self.visuals.widgets.style(response);
|
||||||
if selected {
|
if selected {
|
||||||
|
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
|
||||||
visuals.bg_fill = self.visuals.selection.bg_fill;
|
visuals.bg_fill = self.visuals.selection.bg_fill;
|
||||||
// visuals.bg_stroke = self.visuals.selection.stroke;
|
// visuals.bg_stroke = self.visuals.selection.stroke;
|
||||||
visuals.fg_stroke = self.visuals.selection.stroke;
|
visuals.fg_stroke = self.visuals.selection.stroke;
|
||||||
|
@ -264,8 +268,11 @@ pub struct Spacing {
|
||||||
/// Anything clickable should be (at least) this size.
|
/// Anything clickable should be (at least) this size.
|
||||||
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
||||||
|
|
||||||
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
|
/// Default width of a [`Slider`].
|
||||||
pub slider_width: f32, // TODO(emilk): rename big_interact_size ?
|
pub slider_width: f32,
|
||||||
|
|
||||||
|
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
|
||||||
|
pub combo_width: f32,
|
||||||
|
|
||||||
/// Default width of a [`TextEdit`].
|
/// Default width of a [`TextEdit`].
|
||||||
pub text_edit_width: f32,
|
pub text_edit_width: f32,
|
||||||
|
@ -293,8 +300,12 @@ pub struct Spacing {
|
||||||
|
|
||||||
pub scroll_bar_width: f32,
|
pub scroll_bar_width: f32,
|
||||||
|
|
||||||
|
/// Make sure the scroll handle is at least this big
|
||||||
|
pub scroll_handle_min_length: f32,
|
||||||
|
|
||||||
/// Margin between contents and scroll bar.
|
/// Margin between contents and scroll bar.
|
||||||
pub scroll_bar_inner_margin: f32,
|
pub scroll_bar_inner_margin: f32,
|
||||||
|
|
||||||
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
||||||
pub scroll_bar_outer_margin: f32,
|
pub scroll_bar_outer_margin: f32,
|
||||||
}
|
}
|
||||||
|
@ -481,6 +492,7 @@ pub struct Visuals {
|
||||||
pub resize_corner_size: f32,
|
pub resize_corner_size: f32,
|
||||||
|
|
||||||
pub text_cursor_width: f32,
|
pub text_cursor_width: f32,
|
||||||
|
|
||||||
/// show where the text cursor would be if you clicked
|
/// show where the text cursor would be if you clicked
|
||||||
pub text_cursor_preview: bool,
|
pub text_cursor_preview: bool,
|
||||||
|
|
||||||
|
@ -493,9 +505,17 @@ pub struct Visuals {
|
||||||
/// Show a background behind collapsing headers.
|
/// Show a background behind collapsing headers.
|
||||||
pub collapsing_header_frame: bool,
|
pub collapsing_header_frame: bool,
|
||||||
|
|
||||||
|
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
|
||||||
|
pub indent_has_left_vline: bool,
|
||||||
|
|
||||||
/// Wether or not Grids and Tables should be striped by default
|
/// Wether or not Grids and Tables should be striped by default
|
||||||
/// (have alternating rows differently colored).
|
/// (have alternating rows differently colored).
|
||||||
pub striped: bool,
|
pub striped: bool,
|
||||||
|
|
||||||
|
/// Show trailing color behind the circle of a [`Slider`]. Default is OFF.
|
||||||
|
///
|
||||||
|
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
|
||||||
|
pub slider_trailing_fill: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visuals {
|
impl Visuals {
|
||||||
|
@ -533,7 +553,7 @@ impl Visuals {
|
||||||
// TODO(emilk): replace with an alpha
|
// TODO(emilk): replace with an alpha
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn fade_out_to_color(&self) -> Color32 {
|
pub fn fade_out_to_color(&self) -> Color32 {
|
||||||
self.widgets.noninteractive.bg_fill
|
self.widgets.noninteractive.weak_bg_fill
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returned a "grayed out" version of the given color.
|
/// Returned a "grayed out" version of the given color.
|
||||||
|
@ -566,7 +586,9 @@ pub struct Widgets {
|
||||||
/// The style of an interactive widget, such as a button, at rest.
|
/// The style of an interactive widget, such as a button, at rest.
|
||||||
pub inactive: WidgetVisuals,
|
pub inactive: WidgetVisuals,
|
||||||
|
|
||||||
/// The style of an interactive widget while you hover it.
|
/// The style of an interactive widget while you hover it, or when it is highlighted.
|
||||||
|
///
|
||||||
|
/// See [`Response::hovered`], [`Response::highlighted`] and [`Response::highlight`].
|
||||||
pub hovered: WidgetVisuals,
|
pub hovered: WidgetVisuals,
|
||||||
|
|
||||||
/// The style of an interactive widget as you are clicking or dragging it.
|
/// The style of an interactive widget as you are clicking or dragging it.
|
||||||
|
@ -582,7 +604,7 @@ impl Widgets {
|
||||||
&self.noninteractive
|
&self.noninteractive
|
||||||
} else if response.is_pointer_button_down_on() || response.has_focus() {
|
} else if response.is_pointer_button_down_on() || response.has_focus() {
|
||||||
&self.active
|
&self.active
|
||||||
} else if response.hovered() {
|
} else if response.hovered() || response.highlighted() {
|
||||||
&self.hovered
|
&self.hovered
|
||||||
} else {
|
} else {
|
||||||
&self.inactive
|
&self.inactive
|
||||||
|
@ -594,9 +616,17 @@ impl Widgets {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct WidgetVisuals {
|
pub struct WidgetVisuals {
|
||||||
/// Background color of widget.
|
/// Background color of widgets that must have a background fill,
|
||||||
|
/// such as the slider background, a checkbox background, or a radio button background.
|
||||||
|
///
|
||||||
|
/// Must never be [`Color32::TRANSPARENT`].
|
||||||
pub bg_fill: Color32,
|
pub bg_fill: Color32,
|
||||||
|
|
||||||
|
/// Background color of widgets that can _optionally_ have a background fill, such as buttons.
|
||||||
|
///
|
||||||
|
/// May be [`Color32::TRANSPARENT`].
|
||||||
|
pub weak_bg_fill: Color32,
|
||||||
|
|
||||||
/// For surrounding rectangle of things that need it,
|
/// For surrounding rectangle of things that need it,
|
||||||
/// like buttons, the box of the checkbox, etc.
|
/// like buttons, the box of the checkbox, etc.
|
||||||
/// Should maybe be called `frame_stroke`.
|
/// Should maybe be called `frame_stroke`.
|
||||||
|
@ -663,6 +693,7 @@ impl Default for Style {
|
||||||
override_font_id: None,
|
override_font_id: None,
|
||||||
override_text_style: None,
|
override_text_style: None,
|
||||||
text_styles: default_text_styles(),
|
text_styles: default_text_styles(),
|
||||||
|
drag_value_text_style: TextStyle::Button,
|
||||||
wrap: None,
|
wrap: None,
|
||||||
spacing: Spacing::default(),
|
spacing: Spacing::default(),
|
||||||
interaction: Interaction::default(),
|
interaction: Interaction::default(),
|
||||||
|
@ -684,6 +715,7 @@ impl Default for Spacing {
|
||||||
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
||||||
interact_size: vec2(40.0, 18.0),
|
interact_size: vec2(40.0, 18.0),
|
||||||
slider_width: 100.0,
|
slider_width: 100.0,
|
||||||
|
combo_width: 100.0,
|
||||||
text_edit_width: 280.0,
|
text_edit_width: 280.0,
|
||||||
icon_width: 14.0,
|
icon_width: 14.0,
|
||||||
icon_width_inner: 8.0,
|
icon_width_inner: 8.0,
|
||||||
|
@ -691,6 +723,7 @@ impl Default for Spacing {
|
||||||
tooltip_width: 600.0,
|
tooltip_width: 600.0,
|
||||||
combo_height: 200.0,
|
combo_height: 200.0,
|
||||||
scroll_bar_width: 8.0,
|
scroll_bar_width: 8.0,
|
||||||
|
scroll_handle_min_length: 12.0,
|
||||||
scroll_bar_inner_margin: 4.0,
|
scroll_bar_inner_margin: 4.0,
|
||||||
scroll_bar_outer_margin: 0.0,
|
scroll_bar_outer_margin: 0.0,
|
||||||
indent_ends_with_horizontal_line: false,
|
indent_ends_with_horizontal_line: false,
|
||||||
|
@ -717,8 +750,8 @@ impl Visuals {
|
||||||
widgets: Widgets::default(),
|
widgets: Widgets::default(),
|
||||||
selection: Selection::default(),
|
selection: Selection::default(),
|
||||||
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
||||||
faint_bg_color: Color32::from_gray(35),
|
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||||
code_bg_color: Color32::from_gray(64),
|
code_bg_color: Color32::from_gray(64),
|
||||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||||
|
@ -739,8 +772,11 @@ impl Visuals {
|
||||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||||
button_frame: true,
|
button_frame: true,
|
||||||
collapsing_header_frame: false,
|
collapsing_header_frame: false,
|
||||||
|
indent_has_left_vline: true,
|
||||||
|
|
||||||
striped: false,
|
striped: false,
|
||||||
|
|
||||||
|
slider_trailing_fill: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -751,8 +787,8 @@ impl Visuals {
|
||||||
widgets: Widgets::light(),
|
widgets: Widgets::light(),
|
||||||
selection: Selection::light(),
|
selection: Selection::light(),
|
||||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||||
faint_bg_color: Color32::from_gray(242),
|
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||||
code_bg_color: Color32::from_gray(230),
|
code_bg_color: Color32::from_gray(230),
|
||||||
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
||||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||||
|
@ -801,6 +837,7 @@ impl Widgets {
|
||||||
pub fn dark() -> Self {
|
pub fn dark() -> Self {
|
||||||
Self {
|
Self {
|
||||||
noninteractive: WidgetVisuals {
|
noninteractive: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(27),
|
||||||
bg_fill: Color32::from_gray(27),
|
bg_fill: Color32::from_gray(27),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
||||||
|
@ -808,13 +845,15 @@ impl Widgets {
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
bg_fill: Color32::from_gray(60), // button background
|
weak_bg_fill: Color32::from_gray(60), // button background
|
||||||
|
bg_fill: Color32::from_gray(60), // checkbox background
|
||||||
bg_stroke: Default::default(),
|
bg_stroke: Default::default(),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2.0),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
hovered: WidgetVisuals {
|
hovered: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(70),
|
||||||
bg_fill: Color32::from_gray(70),
|
bg_fill: Color32::from_gray(70),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||||
|
@ -822,6 +861,7 @@ impl Widgets {
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
active: WidgetVisuals {
|
active: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(55),
|
||||||
bg_fill: Color32::from_gray(55),
|
bg_fill: Color32::from_gray(55),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||||
|
@ -829,6 +869,7 @@ impl Widgets {
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
open: WidgetVisuals {
|
open: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(27),
|
||||||
bg_fill: Color32::from_gray(27),
|
bg_fill: Color32::from_gray(27),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||||
|
@ -841,6 +882,7 @@ impl Widgets {
|
||||||
pub fn light() -> Self {
|
pub fn light() -> Self {
|
||||||
Self {
|
Self {
|
||||||
noninteractive: WidgetVisuals {
|
noninteractive: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(248),
|
||||||
bg_fill: Color32::from_gray(248),
|
bg_fill: Color32::from_gray(248),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||||
|
@ -848,13 +890,15 @@ impl Widgets {
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
inactive: WidgetVisuals {
|
inactive: WidgetVisuals {
|
||||||
bg_fill: Color32::from_gray(230), // button background
|
weak_bg_fill: Color32::from_gray(230), // button background
|
||||||
|
bg_fill: Color32::from_gray(230), // checkbox background
|
||||||
bg_stroke: Default::default(),
|
bg_stroke: Default::default(),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||||
rounding: Rounding::same(2.0),
|
rounding: Rounding::same(2.0),
|
||||||
expansion: 0.0,
|
expansion: 0.0,
|
||||||
},
|
},
|
||||||
hovered: WidgetVisuals {
|
hovered: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(220),
|
||||||
bg_fill: Color32::from_gray(220),
|
bg_fill: Color32::from_gray(220),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||||
|
@ -862,6 +906,7 @@ impl Widgets {
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
active: WidgetVisuals {
|
active: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(165),
|
||||||
bg_fill: Color32::from_gray(165),
|
bg_fill: Color32::from_gray(165),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||||
|
@ -869,6 +914,7 @@ impl Widgets {
|
||||||
expansion: 1.0,
|
expansion: 1.0,
|
||||||
},
|
},
|
||||||
open: WidgetVisuals {
|
open: WidgetVisuals {
|
||||||
|
weak_bg_fill: Color32::from_gray(220),
|
||||||
bg_fill: Color32::from_gray(220),
|
bg_fill: Color32::from_gray(220),
|
||||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||||
|
@ -895,6 +941,7 @@ impl Style {
|
||||||
override_font_id,
|
override_font_id,
|
||||||
override_text_style,
|
override_text_style,
|
||||||
text_styles,
|
text_styles,
|
||||||
|
drag_value_text_style,
|
||||||
wrap: _,
|
wrap: _,
|
||||||
spacing,
|
spacing,
|
||||||
interaction,
|
interaction,
|
||||||
|
@ -936,6 +983,19 @@ impl Style {
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Text style of DragValue:");
|
||||||
|
crate::ComboBox::from_id_source("drag_value_text_style")
|
||||||
|
.selected_text(drag_value_text_style.to_string())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
let all_text_styles = ui.style().text_styles();
|
||||||
|
for style in all_text_styles {
|
||||||
|
let text =
|
||||||
|
crate::RichText::new(style.to_string()).text_style(style.clone());
|
||||||
|
ui.selectable_value(drag_value_text_style, style, text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Animation duration:");
|
ui.label("Animation duration:");
|
||||||
ui.add(
|
ui.add(
|
||||||
Slider::new(animation_time, 0.0..=1.0)
|
Slider::new(animation_time, 0.0..=1.0)
|
||||||
|
@ -984,6 +1044,7 @@ impl Spacing {
|
||||||
indent,
|
indent,
|
||||||
interact_size,
|
interact_size,
|
||||||
slider_width,
|
slider_width,
|
||||||
|
combo_width,
|
||||||
text_edit_width,
|
text_edit_width,
|
||||||
icon_width,
|
icon_width,
|
||||||
icon_width_inner,
|
icon_width_inner,
|
||||||
|
@ -992,6 +1053,7 @@ impl Spacing {
|
||||||
indent_ends_with_horizontal_line,
|
indent_ends_with_horizontal_line,
|
||||||
combo_height,
|
combo_height,
|
||||||
scroll_bar_width,
|
scroll_bar_width,
|
||||||
|
scroll_handle_min_length,
|
||||||
scroll_bar_inner_margin,
|
scroll_bar_inner_margin,
|
||||||
scroll_bar_outer_margin,
|
scroll_bar_outer_margin,
|
||||||
} = self;
|
} = self;
|
||||||
|
@ -1012,6 +1074,10 @@ impl Spacing {
|
||||||
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
|
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
|
||||||
ui.label("Slider width");
|
ui.label("Slider width");
|
||||||
});
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
|
||||||
|
ui.label("ComboBox width");
|
||||||
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
|
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
|
||||||
ui.label("TextEdit width");
|
ui.label("TextEdit width");
|
||||||
|
@ -1020,6 +1086,10 @@ impl Spacing {
|
||||||
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
|
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
|
||||||
ui.label("Scroll-bar width");
|
ui.label("Scroll-bar width");
|
||||||
});
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0));
|
||||||
|
ui.label("Scroll-bar handle min length");
|
||||||
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
|
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
|
||||||
ui.label("Scroll-bar inner margin");
|
ui.label("Scroll-bar inner margin");
|
||||||
|
@ -1185,13 +1255,17 @@ impl Selection {
|
||||||
impl WidgetVisuals {
|
impl WidgetVisuals {
|
||||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||||
let Self {
|
let Self {
|
||||||
bg_fill,
|
weak_bg_fill,
|
||||||
|
bg_fill: mandatory_bg_fill,
|
||||||
bg_stroke,
|
bg_stroke,
|
||||||
rounding,
|
rounding,
|
||||||
fg_stroke,
|
fg_stroke,
|
||||||
expansion,
|
expansion,
|
||||||
} = self;
|
} = self;
|
||||||
ui_color(ui, bg_fill, "background fill");
|
ui_color(ui, weak_bg_fill, "optional background fill")
|
||||||
|
.on_hover_text("For buttons, combo-boxes, etc");
|
||||||
|
ui_color(ui, mandatory_bg_fill, "mandatory background fill")
|
||||||
|
.on_hover_text("For checkboxes, sliders, etc");
|
||||||
stroke_ui(ui, bg_stroke, "background stroke");
|
stroke_ui(ui, bg_stroke, "background stroke");
|
||||||
|
|
||||||
rounding_ui(ui, rounding);
|
rounding_ui(ui, rounding);
|
||||||
|
@ -1265,12 +1339,15 @@ impl Visuals {
|
||||||
clip_rect_margin,
|
clip_rect_margin,
|
||||||
button_frame,
|
button_frame,
|
||||||
collapsing_header_frame,
|
collapsing_header_frame,
|
||||||
|
indent_has_left_vline,
|
||||||
|
|
||||||
striped,
|
striped,
|
||||||
|
|
||||||
|
slider_trailing_fill,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.collapsing("Background Colors", |ui| {
|
ui.collapsing("Background Colors", |ui| {
|
||||||
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
|
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
||||||
ui_color(ui, window_fill, "Windows");
|
ui_color(ui, window_fill, "Windows");
|
||||||
ui_color(ui, panel_fill, "Panels");
|
ui_color(ui, panel_fill, "Panels");
|
||||||
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
||||||
|
@ -1322,9 +1399,15 @@ impl Visuals {
|
||||||
|
|
||||||
ui.checkbox(button_frame, "Button has a frame");
|
ui.checkbox(button_frame, "Button has a frame");
|
||||||
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
||||||
|
ui.checkbox(
|
||||||
|
indent_has_left_vline,
|
||||||
|
"Paint a vertical line to the left of indented regions",
|
||||||
|
);
|
||||||
|
|
||||||
ui.checkbox(striped, "By default, add stripes to grids and tables?");
|
ui.checkbox(striped, "By default, add stripes to grids and tables?");
|
||||||
|
|
||||||
|
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
|
||||||
|
|
||||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use epaint::mutex::RwLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||||
widgets::*, *,
|
util::IdTypeMap, widgets::*, *,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -314,84 +314,9 @@ impl Ui {
|
||||||
self.painter().layer_id()
|
self.painter().layer_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
|
||||||
/// Equivalent to `.ctx().input()`.
|
|
||||||
///
|
|
||||||
/// Note that this locks the [`Context`], so be careful with if-let bindings:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
|
|
||||||
/// // This is fine!
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let pos = ui.input().pointer.hover_pos();
|
|
||||||
/// if let Some(pos) = pos {
|
|
||||||
/// // This is also fine!
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// if let Some(pos) = ui.input().pointer.hover_pos() {
|
|
||||||
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
|
|
||||||
self.ctx().input()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
|
||||||
/// Equivalent to `.ctx().input_mut()`.
|
|
||||||
///
|
|
||||||
/// Note that this locks the [`Context`], so be careful with if-let bindings
|
|
||||||
/// like for [`Self::input()`].
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
|
|
||||||
self.ctx().input_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [`Memory`] of the [`Context`] associated with this ui.
|
|
||||||
/// Equivalent to `.ctx().memory()`.
|
|
||||||
#[inline]
|
|
||||||
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
|
|
||||||
self.ctx().memory()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores superficial widget state.
|
|
||||||
#[inline]
|
|
||||||
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
|
|
||||||
self.ctx().data()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [`PlatformOutput`] of the [`Context`] associated with this ui.
|
|
||||||
/// Equivalent to `.ctx().output()`.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// if ui.button("📋").clicked() {
|
|
||||||
/// ui.output().copied_text = "some_text".to_string();
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
#[inline]
|
|
||||||
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
|
|
||||||
self.ctx().output()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [`Fonts`] of the [`Context`] associated with this ui.
|
|
||||||
/// Equivalent to `.ctx().fonts()`.
|
|
||||||
#[inline]
|
|
||||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
|
||||||
self.ctx().fonts()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The height of text of this text style
|
/// The height of text of this text style
|
||||||
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
|
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
|
||||||
self.fonts().row_height(&style.resolve(self.style()))
|
self.fonts(|f| f.row_height(&style.resolve(self.style())))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Screen-space rectangle for clipping what we paint in this ui.
|
/// Screen-space rectangle for clipping what we paint in this ui.
|
||||||
|
@ -413,6 +338,87 @@ impl Ui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Helpers for accessing the underlying [`Context`].
|
||||||
|
/// These functions all lock the [`Context`] owned by this [`Ui`].
|
||||||
|
/// Please see the documentation of [`Context`] for how locking works!
|
||||||
|
impl Ui {
|
||||||
|
/// Read-only access to the shared [`InputState`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// if ui.input(|i| i.key_pressed(egui::Key::A)) {
|
||||||
|
/// // …
|
||||||
|
/// }
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
|
||||||
|
self.ctx().input(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-write access to the shared [`InputState`].
|
||||||
|
#[inline]
|
||||||
|
pub fn input_mut<R>(&self, writer: impl FnOnce(&mut InputState) -> R) -> R {
|
||||||
|
self.ctx().input_mut(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only access to the shared [`Memory`].
|
||||||
|
#[inline]
|
||||||
|
pub fn memory<R>(&self, reader: impl FnOnce(&Memory) -> R) -> R {
|
||||||
|
self.ctx().memory(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-write access to the shared [`Memory`].
|
||||||
|
#[inline]
|
||||||
|
pub fn memory_mut<R>(&self, writer: impl FnOnce(&mut Memory) -> R) -> R {
|
||||||
|
self.ctx().memory_mut(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||||
|
#[inline]
|
||||||
|
pub fn data<R>(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R {
|
||||||
|
self.ctx().data(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-write access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||||
|
#[inline]
|
||||||
|
pub fn data_mut<R>(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R {
|
||||||
|
self.ctx().data_mut(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only access to the shared [`PlatformOutput`].
|
||||||
|
///
|
||||||
|
/// This is what egui outputs each frame.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # let mut ctx = egui::Context::default();
|
||||||
|
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn output<R>(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R {
|
||||||
|
self.ctx().output(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-write access to the shared [`PlatformOutput`].
|
||||||
|
///
|
||||||
|
/// This is what egui outputs each frame.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # let mut ctx = egui::Context::default();
|
||||||
|
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn output_mut<R>(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R {
|
||||||
|
self.ctx().output_mut(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only access to [`Fonts`].
|
||||||
|
#[inline]
|
||||||
|
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||||
|
self.ctx().fonts(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
/// # Sizes etc
|
/// # Sizes etc
|
||||||
|
@ -961,7 +967,8 @@ impl Ui {
|
||||||
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
|
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
let range = rect.min[d]..=rect.max[d];
|
let range = rect.min[d]..=rect.max[d];
|
||||||
self.ctx().frame_state().scroll_target[d] = Some((range, align));
|
self.ctx()
|
||||||
|
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -990,7 +997,8 @@ impl Ui {
|
||||||
let target = self.next_widget_position();
|
let target = self.next_widget_position();
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
let target = target[d];
|
let target = target[d];
|
||||||
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
|
self.ctx()
|
||||||
|
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1022,7 +1030,8 @@ impl Ui {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scroll_with_delta(&self, delta: Vec2) {
|
pub fn scroll_with_delta(&self, delta: Vec2) {
|
||||||
self.ctx().frame_state().scroll_delta += delta;
|
self.ctx()
|
||||||
|
.frame_state_mut(|state| state.scroll_delta += delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1563,7 +1572,7 @@ impl Ui {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Se also [`crate::Image`] and [`crate::ImageButton`].
|
/// See also [`crate::Image`] and [`crate::ImageButton`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
||||||
Image::new(texture_id, size).ui(self)
|
Image::new(texture_id, size).ui(self)
|
||||||
|
@ -1780,24 +1789,31 @@ impl Ui {
|
||||||
};
|
};
|
||||||
let ret = add_contents(&mut child_ui);
|
let ret = add_contents(&mut child_ui);
|
||||||
|
|
||||||
|
let left_vline = self.visuals().indent_has_left_vline;
|
||||||
let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
|
let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
|
||||||
|
|
||||||
if end_with_horizontal_line {
|
if left_vline || end_with_horizontal_line {
|
||||||
child_ui.add_space(4.0);
|
if end_with_horizontal_line {
|
||||||
}
|
child_ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
|
||||||
// draw a faint line on the left to mark the indented section
|
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
||||||
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
||||||
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
let left_top = self.painter().round_pos_to_pixels(left_top);
|
||||||
let left_top = self.painter().round_pos_to_pixels(left_top);
|
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
||||||
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
||||||
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
|
||||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
if left_vline {
|
||||||
if end_with_horizontal_line {
|
// draw a faint line on the left to mark the indented section
|
||||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||||
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
}
|
||||||
self.painter
|
|
||||||
.line_segment([left_bottom, right_bottom], stroke);
|
if end_with_horizontal_line {
|
||||||
|
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
||||||
|
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||||
|
self.painter
|
||||||
|
.line_segment([left_bottom, right_bottom], stroke);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
|
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
|
||||||
|
@ -2157,6 +2173,43 @@ impl Ui {
|
||||||
menu::menu_button(self, title, add_contents)
|
menu::menu_button(self, title, add_contents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a menu button with an image that when clicked will show the given menu.
|
||||||
|
///
|
||||||
|
/// If called from within a menu this will instead create a button for a sub-menu.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use egui_extras;
|
||||||
|
///
|
||||||
|
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
|
||||||
|
/// "rss",
|
||||||
|
/// include_bytes!("rss.svg"),
|
||||||
|
/// egui_extras::image::FitTo::Size(24, 24),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
|
||||||
|
/// ui.menu_button("My sub-menu", |ui| {
|
||||||
|
/// if ui.button("Close the menu").clicked() {
|
||||||
|
/// ui.close_menu();
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||||
|
#[inline]
|
||||||
|
pub fn menu_image_button<R>(
|
||||||
|
&mut self,
|
||||||
|
texture_id: TextureId,
|
||||||
|
image_size: impl Into<Vec2>,
|
||||||
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
|
) -> InnerResponse<Option<R>> {
|
||||||
|
if let Some(menu_state) = self.menu_state.clone() {
|
||||||
|
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||||
|
} else {
|
||||||
|
menu::menu_image_button(self, texture_id, image_size, add_contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -460,18 +460,18 @@ impl IdTypeMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&mut self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.0.is_empty()
|
self.0.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn len(&mut self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count how many values are stored but not yet deserialized.
|
/// Count how many values are stored but not yet deserialized.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn count_serialized(&mut self) -> usize {
|
pub fn count_serialized(&self) -> usize {
|
||||||
self.0
|
self.0
|
||||||
.values()
|
.values()
|
||||||
.filter(|e| matches!(e, Element::Serialized { .. }))
|
.filter(|e| matches!(e, Element::Serialized { .. }))
|
||||||
|
@ -479,7 +479,7 @@ impl IdTypeMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count the number of values are stored with the given type.
|
/// Count the number of values are stored with the given type.
|
||||||
pub fn count<T: 'static>(&mut self) -> usize {
|
pub fn count<T: 'static>(&self) -> usize {
|
||||||
let key = TypeId::of::<T>();
|
let key = TypeId::of::<T>();
|
||||||
self.0
|
self.0
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -573,14 +573,14 @@ impl WidgetText {
|
||||||
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
||||||
text_job.job.wrap.max_width = wrap_width;
|
text_job.job.wrap.max_width = wrap_width;
|
||||||
WidgetTextGalley {
|
WidgetTextGalley {
|
||||||
galley: ui.fonts().layout_job(text_job.job),
|
galley: ui.fonts(|f| f.layout_job(text_job.job)),
|
||||||
galley_has_color: text_job.job_has_color,
|
galley_has_color: text_job.job_has_color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::LayoutJob(mut job) => {
|
Self::LayoutJob(mut job) => {
|
||||||
job.wrap.max_width = wrap_width;
|
job.wrap.max_width = wrap_width;
|
||||||
WidgetTextGalley {
|
WidgetTextGalley {
|
||||||
galley: ui.fonts().layout_job(job),
|
galley: ui.fonts(|f| f.layout_job(job)),
|
||||||
galley_has_color: true,
|
galley_has_color: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ pub struct Button {
|
||||||
small: bool,
|
small: bool,
|
||||||
frame: Option<bool>,
|
frame: Option<bool>,
|
||||||
min_size: Vec2,
|
min_size: Vec2,
|
||||||
|
rounding: Option<Rounding>,
|
||||||
image: Option<widgets::Image>,
|
image: Option<widgets::Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ impl Button {
|
||||||
small: false,
|
small: false,
|
||||||
frame: None,
|
frame: None,
|
||||||
min_size: Vec2::ZERO,
|
min_size: Vec2::ZERO,
|
||||||
|
rounding: None,
|
||||||
image: None,
|
image: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +119,12 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the rounding of the button.
|
||||||
|
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||||
|
self.rounding = Some(rounding.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Show some text on the right side of the button, in weak color.
|
/// Show some text on the right side of the button, in weak color.
|
||||||
///
|
///
|
||||||
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
||||||
|
@ -140,6 +148,7 @@ impl Widget for Button {
|
||||||
small,
|
small,
|
||||||
frame,
|
frame,
|
||||||
min_size,
|
min_size,
|
||||||
|
rounding,
|
||||||
image,
|
image,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -171,10 +180,10 @@ impl Widget for Button {
|
||||||
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
||||||
desired_size.y = desired_size.y.max(shortcut_text.size().y);
|
desired_size.y = desired_size.y.max(shortcut_text.size().y);
|
||||||
}
|
}
|
||||||
|
desired_size += 2.0 * button_padding;
|
||||||
if !small {
|
if !small {
|
||||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||||
}
|
}
|
||||||
desired_size += 2.0 * button_padding;
|
|
||||||
desired_size = desired_size.at_least(min_size);
|
desired_size = desired_size.at_least(min_size);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||||
|
@ -184,14 +193,11 @@ impl Widget for Button {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
if frame {
|
if frame {
|
||||||
let fill = fill.unwrap_or(visuals.bg_fill);
|
let fill = fill.unwrap_or(visuals.weak_bg_fill);
|
||||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
||||||
ui.painter().rect(
|
let rounding = rounding.unwrap_or(visuals.rounding);
|
||||||
rect.expand(visuals.expansion),
|
ui.painter()
|
||||||
visuals.rounding,
|
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
||||||
fill,
|
|
||||||
stroke,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_pos = if let Some(image) = image {
|
let text_pos = if let Some(image) = image {
|
||||||
|
@ -221,7 +227,10 @@ impl Widget for Button {
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
let image_rect = Rect::from_min_size(
|
let image_rect = Rect::from_min_size(
|
||||||
pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)),
|
pos2(
|
||||||
|
rect.min.x + button_padding.x,
|
||||||
|
rect.center().y - 0.5 - (image.size().y / 2.0),
|
||||||
|
),
|
||||||
image.size(),
|
image.size(),
|
||||||
);
|
);
|
||||||
image.paint_at(ui, image_rect);
|
image.paint_at(ui, image_rect);
|
||||||
|
@ -260,6 +269,10 @@ impl<'a> Checkbox<'a> {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn without_text(checked: &'a mut bool) -> Self {
|
||||||
|
Self::new(checked, WidgetText::default())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Checkbox<'a> {
|
impl<'a> Widget for Checkbox<'a> {
|
||||||
|
@ -531,7 +544,7 @@ impl Widget for ImageButton {
|
||||||
(
|
(
|
||||||
expansion,
|
expansion,
|
||||||
visuals.rounding,
|
visuals.rounding,
|
||||||
visuals.bg_fill,
|
visuals.weak_bg_fill,
|
||||||
visuals.bg_stroke,
|
visuals.bg_stroke,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -228,9 +228,9 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
|
||||||
|
|
||||||
if ui.button("📋").on_hover_text("Click to copy").clicked() {
|
if ui.button("📋").on_hover_text("Click to copy").clicked() {
|
||||||
if alpha == Alpha::Opaque {
|
if alpha == Alpha::Opaque {
|
||||||
ui.output().copied_text = format!("{}, {}, {}", r, g, b);
|
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
|
||||||
} else {
|
} else {
|
||||||
ui.output().copied_text = format!("{}, {}, {}, {}", r, g, b, a);
|
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,20 +341,20 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
|
||||||
|
|
||||||
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
|
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
|
||||||
let popup_id = ui.auto_id_with("popup");
|
let popup_id = ui.auto_id_with("popup");
|
||||||
let open = ui.memory().is_popup_open(popup_id);
|
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
|
||||||
let mut button_response = color_button(ui, (*hsva).into(), open);
|
let mut button_response = color_button(ui, (*hsva).into(), open);
|
||||||
if ui.style().explanation_tooltips {
|
if ui.style().explanation_tooltips {
|
||||||
button_response = button_response.on_hover_text("Click to edit color");
|
button_response = button_response.on_hover_text("Click to edit color");
|
||||||
}
|
}
|
||||||
|
|
||||||
if button_response.clicked() {
|
if button_response.clicked() {
|
||||||
ui.memory().toggle_popup(popup_id);
|
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLOR_SLIDER_WIDTH: f32 = 210.0;
|
const COLOR_SLIDER_WIDTH: f32 = 210.0;
|
||||||
|
|
||||||
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
|
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
|
||||||
if ui.memory().is_popup_open(popup_id) {
|
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||||
let area_response = Area::new(popup_id)
|
let area_response = Area::new(popup_id)
|
||||||
.order(Order::Foreground)
|
.order(Order::Foreground)
|
||||||
.fixed_pos(button_response.rect.max)
|
.fixed_pos(button_response.rect.max)
|
||||||
|
@ -370,9 +370,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
|
||||||
.response;
|
.response;
|
||||||
|
|
||||||
if !button_response.clicked()
|
if !button_response.clicked()
|
||||||
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
|
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
|
||||||
{
|
{
|
||||||
ui.memory().close_popup();
|
ui.memory_mut(|mem| mem.close_popup());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,5 +436,5 @@ fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
|
||||||
|
|
||||||
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
|
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
|
||||||
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
|
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
|
||||||
f(ctx.data().get_temp_mut_or_default(Id::null()))
|
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
|
||||||
}
|
}
|
||||||
|
|
|
@ -368,33 +368,35 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
custom_parser,
|
custom_parser,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let shift = ui.input().modifiers.shift_only();
|
let shift = ui.input(|i| i.modifiers.shift_only());
|
||||||
// The widget has the same ID whether it's in edit or button mode.
|
// The widget has the same ID whether it's in edit or button mode.
|
||||||
let id = ui.next_auto_id();
|
let id = ui.next_auto_id();
|
||||||
let is_slow_speed = shift && ui.memory().is_being_dragged(id);
|
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
|
||||||
|
|
||||||
// The following call ensures that when a `DragValue` receives focus,
|
// The following ensures that when a `DragValue` receives focus,
|
||||||
// it is immediately rendered in edit mode, rather than being rendered
|
// it is immediately rendered in edit mode, rather than being rendered
|
||||||
// in button mode for just one frame. This is important for
|
// in button mode for just one frame. This is important for
|
||||||
// screen readers.
|
// screen readers.
|
||||||
ui.memory().interested_in_focus(id);
|
let is_kb_editing = ui.memory_mut(|mem| {
|
||||||
let is_kb_editing = ui.memory().has_focus(id);
|
mem.interested_in_focus(id);
|
||||||
if ui.memory().gained_focus(id) {
|
let is_kb_editing = mem.has_focus(id);
|
||||||
ui.memory().drag_value.edit_string = None;
|
if mem.gained_focus(id) {
|
||||||
}
|
mem.drag_value.edit_string = None;
|
||||||
|
}
|
||||||
|
is_kb_editing
|
||||||
|
});
|
||||||
|
|
||||||
let old_value = get(&mut get_set_value);
|
let old_value = get(&mut get_set_value);
|
||||||
let mut value = old_value;
|
let mut value = old_value;
|
||||||
let aim_rad = ui.input().aim_radius() as f64;
|
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 = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
|
||||||
let auto_decimals = auto_decimals + is_slow_speed as usize;
|
let auto_decimals = auto_decimals + is_slow_speed as usize;
|
||||||
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
|
||||||
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
||||||
|
|
||||||
let change = {
|
let change = ui.input_mut(|input| {
|
||||||
let mut change = 0.0;
|
let mut change = 0.0;
|
||||||
let mut input = ui.input_mut();
|
|
||||||
|
|
||||||
if is_kb_editing {
|
if is_kb_editing {
|
||||||
// This deliberately doesn't listen for left and right arrow keys,
|
// This deliberately doesn't listen for left and right arrow keys,
|
||||||
|
@ -418,16 +420,18 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
change
|
change
|
||||||
};
|
});
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
use accesskit::{Action, ActionData};
|
use accesskit::{Action, ActionData};
|
||||||
for request in ui.input().accesskit_action_requests(id, Action::SetValue) {
|
ui.input(|input| {
|
||||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
for request in input.accesskit_action_requests(id, Action::SetValue) {
|
||||||
value = new_value;
|
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||||
|
value = new_value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if change != 0.0 {
|
if change != 0.0 {
|
||||||
|
@ -438,7 +442,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
value = clamp_to_range(value, clamp_range.clone());
|
value = clamp_to_range(value, clamp_range.clone());
|
||||||
if old_value != value {
|
if old_value != value {
|
||||||
set(&mut get_set_value, value);
|
set(&mut get_set_value, value);
|
||||||
ui.memory().drag_value.edit_string = None;
|
ui.memory_mut(|mem| mem.drag_value.edit_string = None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let value_text = match custom_formatter {
|
let value_text = match custom_formatter {
|
||||||
|
@ -452,35 +456,43 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let text_style = ui.style().drag_value_text_style.clone();
|
||||||
|
|
||||||
// some clones below are redundant if AccessKit is disabled
|
// some clones below are redundant if AccessKit is disabled
|
||||||
#[allow(clippy::redundant_clone)]
|
#[allow(clippy::redundant_clone)]
|
||||||
let mut response = if is_kb_editing {
|
let mut response = if is_kb_editing {
|
||||||
let button_width = ui.spacing().interact_size.x;
|
|
||||||
let mut value_text = ui
|
let mut value_text = ui
|
||||||
.memory()
|
.memory_mut(|mem| mem.drag_value.edit_string.take())
|
||||||
.drag_value
|
|
||||||
.edit_string
|
|
||||||
.take()
|
|
||||||
.unwrap_or_else(|| value_text.clone());
|
.unwrap_or_else(|| value_text.clone());
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
TextEdit::singleline(&mut value_text)
|
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)
|
.id(id)
|
||||||
.desired_width(button_width)
|
.desired_width(ui.spacing().interact_size.x)
|
||||||
.font(TextStyle::Monospace),
|
.font(text_style),
|
||||||
);
|
);
|
||||||
let parsed_value = match custom_parser {
|
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
|
||||||
Some(parser) => parser(&value_text),
|
// See https://github.com/emilk/egui/issues/2687
|
||||||
None => value_text.parse().ok(),
|
if response.lost_focus() {
|
||||||
};
|
let parsed_value = match custom_parser {
|
||||||
if let Some(parsed_value) = parsed_value {
|
Some(parser) => parser(&value_text),
|
||||||
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
|
None => value_text.parse().ok(),
|
||||||
set(&mut get_set_value, parsed_value);
|
};
|
||||||
|
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().drag_value.edit_string = Some(value_text);
|
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text));
|
||||||
response
|
response
|
||||||
} else {
|
} else {
|
||||||
let button = Button::new(
|
let button = Button::new(
|
||||||
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)).monospace(),
|
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
|
||||||
|
.text_style(text_style),
|
||||||
)
|
)
|
||||||
.wrap(false)
|
.wrap(false)
|
||||||
.sense(Sense::click_and_drag())
|
.sense(Sense::click_and_drag())
|
||||||
|
@ -499,10 +511,18 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
ui.memory().drag_value.edit_string = None;
|
ui.memory_mut(|mem| {
|
||||||
ui.memory().request_focus(id);
|
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() {
|
} else if response.dragged() {
|
||||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||||
|
|
||||||
let mdelta = response.drag_delta();
|
let mdelta = response.drag_delta();
|
||||||
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
|
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
|
||||||
|
@ -512,7 +532,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
let delta_value = delta_points as f64 * speed;
|
let delta_value = delta_points as f64 * speed;
|
||||||
|
|
||||||
if delta_value != 0.0 {
|
if delta_value != 0.0 {
|
||||||
let mut drag_state = std::mem::take(&mut ui.memory().drag_value);
|
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:
|
// 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))
|
let stored_value = (drag_state.last_dragged_id == Some(response.id))
|
||||||
|
@ -533,7 +553,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
|
|
||||||
drag_state.last_dragged_id = Some(response.id);
|
drag_state.last_dragged_id = Some(response.id);
|
||||||
drag_state.last_dragged_value = Some(stored_value);
|
drag_state.last_dragged_value = Some(stored_value);
|
||||||
ui.memory().drag_value = drag_state;
|
ui.memory_mut(|mem| mem.drag_value = drag_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,28 +565,28 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
response.widget_info(|| WidgetInfo::drag_value(value));
|
response.widget_info(|| WidgetInfo::drag_value(value));
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
||||||
use accesskit::Action;
|
use accesskit::Action;
|
||||||
// If either end of the range is unbounded, it's better
|
// If either end of the range is unbounded, it's better
|
||||||
// to leave the corresponding AccessKit field set to None,
|
// to leave the corresponding AccessKit field set to None,
|
||||||
// to allow for platform-specific default behavior.
|
// to allow for platform-specific default behavior.
|
||||||
if clamp_range.start().is_finite() {
|
if clamp_range.start().is_finite() {
|
||||||
node.min_numeric_value = Some(*clamp_range.start());
|
builder.set_min_numeric_value(*clamp_range.start());
|
||||||
}
|
}
|
||||||
if clamp_range.end().is_finite() {
|
if clamp_range.end().is_finite() {
|
||||||
node.max_numeric_value = Some(*clamp_range.end());
|
builder.set_max_numeric_value(*clamp_range.end());
|
||||||
}
|
}
|
||||||
node.numeric_value_step = Some(speed);
|
builder.set_numeric_value_step(speed);
|
||||||
node.actions |= Action::SetValue;
|
builder.add_action(Action::SetValue);
|
||||||
if value < *clamp_range.end() {
|
if value < *clamp_range.end() {
|
||||||
node.actions |= Action::Increment;
|
builder.add_action(Action::Increment);
|
||||||
}
|
}
|
||||||
if value > *clamp_range.start() {
|
if value > *clamp_range.start() {
|
||||||
node.actions |= Action::Decrement;
|
builder.add_action(Action::Decrement);
|
||||||
}
|
}
|
||||||
// The name field is set to the current value by the button,
|
// 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.
|
// but we don't want it set that way on this widget type.
|
||||||
node.name = None;
|
builder.clear_name();
|
||||||
// Always expose the value as a string. This makes the widget
|
// Always expose the value as a string. This makes the widget
|
||||||
// more stable to accessibility users as it switches
|
// more stable to accessibility users as it switches
|
||||||
// between edit and button modes. This is particularly important
|
// between edit and button modes. This is particularly important
|
||||||
|
@ -587,9 +607,9 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
// when in edit mode.
|
// when in edit mode.
|
||||||
if !is_kb_editing {
|
if !is_kb_editing {
|
||||||
let value_text = format!("{}{}{}", prefix, value_text, suffix);
|
let value_text = format!("{}{}{}", prefix, value_text, suffix);
|
||||||
node.value = Some(value_text.into());
|
builder.set_value(value_text);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ impl Widget for Link {
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
|
||||||
|
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
|
@ -110,16 +110,20 @@ impl Widget for Hyperlink {
|
||||||
|
|
||||||
let response = ui.add(Link::new(text));
|
let response = ui.add(Link::new(text));
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
let modifiers = ui.ctx().input().modifiers;
|
let modifiers = ui.ctx().input(|i| i.modifiers);
|
||||||
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
ui.ctx().output_mut(|o| {
|
||||||
url: url.clone(),
|
o.open_url = Some(crate::output::OpenUrl {
|
||||||
new_tab: modifiers.any(),
|
url: url.clone(),
|
||||||
|
new_tab: modifiers.any(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if response.middle_clicked() {
|
if response.middle_clicked() {
|
||||||
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
ui.ctx().output_mut(|o| {
|
||||||
url: url.clone(),
|
o.open_url = Some(crate::output::OpenUrl {
|
||||||
new_tab: true,
|
url: url.clone(),
|
||||||
|
new_tab: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
response.on_hover_text(url)
|
response.on_hover_text(url)
|
||||||
|
|
|
@ -72,7 +72,7 @@ impl Label {
|
||||||
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
|
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
|
||||||
let sense = self.sense.unwrap_or_else(|| {
|
let sense = self.sense.unwrap_or_else(|| {
|
||||||
// We only want to focus labels if the screen reader is on.
|
// We only want to focus labels if the screen reader is on.
|
||||||
if ui.memory().options.screen_reader {
|
if ui.memory(|mem| mem.options.screen_reader) {
|
||||||
Sense::focusable_noninteractive()
|
Sense::focusable_noninteractive()
|
||||||
} else {
|
} else {
|
||||||
Sense::hover()
|
Sense::hover()
|
||||||
|
@ -120,7 +120,7 @@ impl Label {
|
||||||
if let Some(first_section) = text_job.job.sections.first_mut() {
|
if let Some(first_section) = text_job.job.sections.first_mut() {
|
||||||
first_section.leading_space = first_row_indentation;
|
first_section.leading_space = first_row_indentation;
|
||||||
}
|
}
|
||||||
let text_galley = text_job.into_galley(&ui.fonts());
|
let text_galley = ui.fonts(|f| text_job.into_galley(f));
|
||||||
|
|
||||||
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -153,7 +153,7 @@ impl Label {
|
||||||
text_job.job.justify = ui.layout().horizontal_justify();
|
text_job.job.justify = ui.layout().horizontal_justify();
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_galley = text_job.into_galley(&ui.fonts());
|
let text_galley = ui.fonts(|f| text_job.into_galley(f));
|
||||||
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
|
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
|
||||||
let pos = match text_galley.galley.job.halign {
|
let pos = match text_galley.galley.job.halign {
|
||||||
Align::LEFT => rect.left_top(),
|
Align::LEFT => rect.left_top(),
|
||||||
|
@ -173,7 +173,7 @@ impl Widget for Label {
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
let response_color = ui.style().interact(&response).text_color();
|
let response_color = ui.style().interact(&response).text_color();
|
||||||
|
|
||||||
let underline = if response.has_focus() {
|
let underline = if response.has_focus() || response.highlighted() {
|
||||||
Stroke::new(1.0, response_color)
|
Stroke::new(1.0, response_color)
|
||||||
} else {
|
} else {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
|
|
|
@ -1658,14 +1658,16 @@ fn add_rulers_and_text(
|
||||||
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
||||||
|
|
||||||
let corner_value = elem.corner_value();
|
let corner_value = elem.corner_value();
|
||||||
shapes.push(Shape::text(
|
plot.ui.fonts(|f| {
|
||||||
&plot.ui.fonts(),
|
shapes.push(Shape::text(
|
||||||
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
|
f,
|
||||||
Align2::LEFT_BOTTOM,
|
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
|
||||||
text,
|
Align2::LEFT_BOTTOM,
|
||||||
font_id,
|
text,
|
||||||
plot.ui.visuals().text_color(),
|
font_id,
|
||||||
));
|
plot.ui.visuals().text_color(),
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
||||||
|
@ -1714,15 +1716,16 @@ pub(super) fn rulers_at_value(
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
||||||
|
plot.ui.fonts(|f| {
|
||||||
shapes.push(Shape::text(
|
shapes.push(Shape::text(
|
||||||
&plot.ui.fonts(),
|
f,
|
||||||
pointer + vec2(3.0, -2.0),
|
pointer + vec2(3.0, -2.0),
|
||||||
Align2::LEFT_BOTTOM,
|
Align2::LEFT_BOTTOM,
|
||||||
text,
|
text,
|
||||||
font_id,
|
font_id,
|
||||||
plot.ui.visuals().text_color(),
|
plot.ui.visuals().text_color(),
|
||||||
));
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_closest_rect<'a, T>(
|
fn find_closest_rect<'a, T>(
|
||||||
|
|
|
@ -89,9 +89,7 @@ impl LegendEntry {
|
||||||
|
|
||||||
let font_id = text_style.resolve(ui.style());
|
let font_id = text_style.resolve(ui.style());
|
||||||
|
|
||||||
let galley = ui
|
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
|
||||||
.fonts()
|
|
||||||
.layout_delayed_color(text, font_id, f32::INFINITY);
|
|
||||||
|
|
||||||
let icon_size = galley.size().y;
|
let icon_size = galley.size().y;
|
||||||
let icon_spacing = icon_size / 5.0;
|
let icon_spacing = icon_size / 5.0;
|
||||||
|
|
|
@ -108,11 +108,11 @@ struct PlotMemory {
|
||||||
|
|
||||||
impl PlotMemory {
|
impl PlotMemory {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data().get_persisted(id)
|
ctx.data_mut(|d| d.get_persisted(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data().insert_persisted(id, self);
|
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -742,7 +742,7 @@ impl Plot {
|
||||||
});
|
});
|
||||||
|
|
||||||
let PlotMemory {
|
let PlotMemory {
|
||||||
mut bounds_modified,
|
bounds_modified,
|
||||||
mut hovered_entry,
|
mut hovered_entry,
|
||||||
mut hidden_items,
|
mut hidden_items,
|
||||||
last_screen_transform,
|
last_screen_transform,
|
||||||
|
@ -754,6 +754,7 @@ impl Plot {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
next_auto_color_idx: 0,
|
next_auto_color_idx: 0,
|
||||||
last_screen_transform,
|
last_screen_transform,
|
||||||
|
bounds_modified,
|
||||||
response,
|
response,
|
||||||
ctx: ui.ctx().clone(),
|
ctx: ui.ctx().clone(),
|
||||||
};
|
};
|
||||||
|
@ -762,6 +763,7 @@ impl Plot {
|
||||||
mut items,
|
mut items,
|
||||||
mut response,
|
mut response,
|
||||||
last_screen_transform,
|
last_screen_transform,
|
||||||
|
mut bounds_modified,
|
||||||
..
|
..
|
||||||
} = plot_ui;
|
} = plot_ui;
|
||||||
|
|
||||||
|
@ -953,9 +955,9 @@ impl Plot {
|
||||||
if let Some(hover_pos) = response.hover_pos() {
|
if let Some(hover_pos) = response.hover_pos() {
|
||||||
if allow_zoom {
|
if allow_zoom {
|
||||||
let zoom_factor = if data_aspect.is_some() {
|
let zoom_factor = if data_aspect.is_some() {
|
||||||
Vec2::splat(ui.input().zoom_delta())
|
Vec2::splat(ui.input(|i| i.zoom_delta()))
|
||||||
} else {
|
} else {
|
||||||
ui.input().zoom_delta_2d()
|
ui.input(|i| i.zoom_delta_2d())
|
||||||
};
|
};
|
||||||
if zoom_factor != Vec2::splat(1.0) {
|
if zoom_factor != Vec2::splat(1.0) {
|
||||||
transform.zoom(zoom_factor, hover_pos);
|
transform.zoom(zoom_factor, hover_pos);
|
||||||
|
@ -963,7 +965,7 @@ impl Plot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allow_scroll {
|
if allow_scroll {
|
||||||
let scroll_delta = ui.input().scroll_delta;
|
let scroll_delta = ui.input(|i| i.scroll_delta);
|
||||||
if scroll_delta != Vec2::ZERO {
|
if scroll_delta != Vec2::ZERO {
|
||||||
transform.translate_bounds(-scroll_delta);
|
transform.translate_bounds(-scroll_delta);
|
||||||
bounds_modified = true.into();
|
bounds_modified = true.into();
|
||||||
|
@ -1042,6 +1044,7 @@ pub struct PlotUi {
|
||||||
items: Vec<Box<dyn PlotItem>>,
|
items: Vec<Box<dyn PlotItem>>,
|
||||||
next_auto_color_idx: usize,
|
next_auto_color_idx: usize,
|
||||||
last_screen_transform: ScreenTransform,
|
last_screen_transform: ScreenTransform,
|
||||||
|
bounds_modified: AxisBools,
|
||||||
response: Response,
|
response: Response,
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
}
|
}
|
||||||
|
@ -1069,11 +1072,13 @@ impl PlotUi {
|
||||||
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||||
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
|
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
|
||||||
self.last_screen_transform.set_bounds(plot_bounds);
|
self.last_screen_transform.set_bounds(plot_bounds);
|
||||||
|
self.bounds_modified = true.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||||
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
|
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
|
||||||
self.last_screen_transform.translate_bounds(delta_pos);
|
self.last_screen_transform.translate_bounds(delta_pos);
|
||||||
|
self.bounds_modified = true.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the plot area is currently hovered.
|
/// Returns `true` if the plot area is currently hovered.
|
||||||
|
@ -1094,7 +1099,7 @@ impl PlotUi {
|
||||||
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
|
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
|
||||||
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
|
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
|
||||||
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
|
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
|
||||||
let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta();
|
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
|
||||||
let value = self.plot_from_screen(last_pos);
|
let value = self.plot_from_screen(last_pos);
|
||||||
Some(value)
|
Some(value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub struct ProgressBar {
|
||||||
progress: f32,
|
progress: f32,
|
||||||
desired_width: Option<f32>,
|
desired_width: Option<f32>,
|
||||||
text: Option<ProgressBarText>,
|
text: Option<ProgressBarText>,
|
||||||
|
fill: Option<Color32>,
|
||||||
animate: bool,
|
animate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ impl ProgressBar {
|
||||||
progress: progress.clamp(0.0, 1.0),
|
progress: progress.clamp(0.0, 1.0),
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
text: None,
|
text: None,
|
||||||
|
fill: None,
|
||||||
animate: false,
|
animate: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +35,12 @@ impl ProgressBar {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The fill color of the bar.
|
||||||
|
pub fn fill(mut self, color: Color32) -> Self {
|
||||||
|
self.fill = Some(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// A custom text to display on the progress bar.
|
/// A custom text to display on the progress bar.
|
||||||
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
|
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
|
||||||
self.text = Some(ProgressBarText::Custom(text.into()));
|
self.text = Some(ProgressBarText::Custom(text.into()));
|
||||||
|
@ -60,6 +68,7 @@ impl Widget for ProgressBar {
|
||||||
progress,
|
progress,
|
||||||
desired_width,
|
desired_width,
|
||||||
text,
|
text,
|
||||||
|
fill,
|
||||||
animate,
|
animate,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -90,7 +99,8 @@ impl Widget for ProgressBar {
|
||||||
|
|
||||||
let (dark, bright) = (0.7, 1.0);
|
let (dark, bright) = (0.7, 1.0);
|
||||||
let color_factor = if animate {
|
let color_factor = if animate {
|
||||||
lerp(dark..=bright, ui.input().time.cos().abs())
|
let time = ui.input(|i| i.time);
|
||||||
|
lerp(dark..=bright, time.cos().abs())
|
||||||
} else {
|
} else {
|
||||||
bright
|
bright
|
||||||
};
|
};
|
||||||
|
@ -98,14 +108,17 @@ impl Widget for ProgressBar {
|
||||||
ui.painter().rect(
|
ui.painter().rect(
|
||||||
inner_rect,
|
inner_rect,
|
||||||
rounding,
|
rounding,
|
||||||
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
|
Color32::from(
|
||||||
|
Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
|
||||||
|
),
|
||||||
Stroke::NONE,
|
Stroke::NONE,
|
||||||
);
|
);
|
||||||
|
|
||||||
if animate {
|
if animate {
|
||||||
let n_points = 20;
|
let n_points = 20;
|
||||||
let start_angle = ui.input().time * std::f64::consts::TAU;
|
let time = ui.input(|i| i.time);
|
||||||
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
let start_angle = time * std::f64::consts::TAU;
|
||||||
|
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
||||||
let circle_radius = rounding - 2.0;
|
let circle_radius = rounding - 2.0;
|
||||||
let points: Vec<Pos2> = (0..n_points)
|
let points: Vec<Pos2> = (0..n_points)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
|
@ -116,10 +129,8 @@ impl Widget for ProgressBar {
|
||||||
+ vec2(-rounding, 0.0)
|
+ vec2(-rounding, 0.0)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
ui.painter().add(Shape::line(
|
ui.painter()
|
||||||
points,
|
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
|
||||||
Stroke::new(2.0, visuals.faint_bg_color),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(text_kind) = text {
|
if let Some(text_kind) = text {
|
||||||
|
|
|
@ -61,11 +61,15 @@ impl Widget for SelectableLabel {
|
||||||
|
|
||||||
let visuals = ui.style().interact_selectable(&response, selected);
|
let visuals = ui.style().interact_selectable(&response, selected);
|
||||||
|
|
||||||
if selected || response.hovered() || response.has_focus() {
|
if selected || response.hovered() || response.highlighted() || response.has_focus() {
|
||||||
let rect = rect.expand(visuals.expansion);
|
let rect = rect.expand(visuals.expansion);
|
||||||
|
|
||||||
ui.painter()
|
ui.painter().rect(
|
||||||
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
|
rect,
|
||||||
|
visuals.rounding,
|
||||||
|
visuals.weak_bg_fill,
|
||||||
|
visuals.bg_stroke,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
|
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
|
||||||
|
|
|
@ -14,6 +14,7 @@ use crate::*;
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
pub struct Separator {
|
pub struct Separator {
|
||||||
spacing: f32,
|
spacing: f32,
|
||||||
|
grow: f32,
|
||||||
is_horizontal_line: Option<bool>,
|
is_horizontal_line: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ impl Default for Separator {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
spacing: 6.0,
|
spacing: 6.0,
|
||||||
|
grow: 0.0,
|
||||||
is_horizontal_line: None,
|
is_horizontal_line: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +30,19 @@ impl Default for Separator {
|
||||||
|
|
||||||
impl Separator {
|
impl Separator {
|
||||||
/// How much space we take up. The line is painted in the middle of this.
|
/// How much space we take up. The line is painted in the middle of this.
|
||||||
|
///
|
||||||
|
/// In a vertical layout, with a horizontal Separator,
|
||||||
|
/// this is the height of the separator widget.
|
||||||
|
///
|
||||||
|
/// In a horizontal layout, with a vertical Separator,
|
||||||
|
/// this is the width of the separator widget.
|
||||||
pub fn spacing(mut self, spacing: f32) -> Self {
|
pub fn spacing(mut self, spacing: f32) -> Self {
|
||||||
self.spacing = spacing;
|
self.spacing = spacing;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicitly ask for a horizontal line.
|
/// Explicitly ask for a horizontal line.
|
||||||
|
///
|
||||||
/// By default you will get a horizontal line in vertical layouts,
|
/// By default you will get a horizontal line in vertical layouts,
|
||||||
/// and a vertical line in horizontal layouts.
|
/// and a vertical line in horizontal layouts.
|
||||||
pub fn horizontal(mut self) -> Self {
|
pub fn horizontal(mut self) -> Self {
|
||||||
|
@ -42,18 +51,40 @@ impl Separator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicitly ask for a vertical line.
|
/// Explicitly ask for a vertical line.
|
||||||
|
///
|
||||||
/// By default you will get a horizontal line in vertical layouts,
|
/// By default you will get a horizontal line in vertical layouts,
|
||||||
/// and a vertical line in horizontal layouts.
|
/// and a vertical line in horizontal layouts.
|
||||||
pub fn vertical(mut self) -> Self {
|
pub fn vertical(mut self) -> Self {
|
||||||
self.is_horizontal_line = Some(false);
|
self.is_horizontal_line = Some(false);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extend each end of the separator line by this much.
|
||||||
|
///
|
||||||
|
/// The default is to take up the available width/height of the parent.
|
||||||
|
///
|
||||||
|
/// This will make the line extend outside the parent ui.
|
||||||
|
pub fn grow(mut self, extra: f32) -> Self {
|
||||||
|
self.grow += extra;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contract each end of the separator line by this much.
|
||||||
|
///
|
||||||
|
/// The default is to take up the available width/height of the parent.
|
||||||
|
///
|
||||||
|
/// This effectively adds margins to the line.
|
||||||
|
pub fn shrink(mut self, shrink: f32) -> Self {
|
||||||
|
self.grow -= shrink;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Separator {
|
impl Widget for Separator {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let Separator {
|
let Separator {
|
||||||
spacing,
|
spacing,
|
||||||
|
grow,
|
||||||
is_horizontal_line,
|
is_horizontal_line,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -75,14 +106,14 @@ impl Widget for Separator {
|
||||||
let painter = ui.painter();
|
let painter = ui.painter();
|
||||||
if is_horizontal_line {
|
if is_horizontal_line {
|
||||||
painter.hline(
|
painter.hline(
|
||||||
rect.x_range(),
|
(rect.left() - grow)..=(rect.right() + grow),
|
||||||
painter.round_to_pixel(rect.center().y),
|
painter.round_to_pixel(rect.center().y),
|
||||||
stroke,
|
stroke,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
painter.vline(
|
painter.vline(
|
||||||
painter.round_to_pixel(rect.center().x),
|
painter.round_to_pixel(rect.center().x),
|
||||||
rect.y_range(),
|
(rect.top() - grow)..=(rect.bottom() + grow),
|
||||||
stroke,
|
stroke,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@ pub struct Slider<'a> {
|
||||||
max_decimals: Option<usize>,
|
max_decimals: Option<usize>,
|
||||||
custom_formatter: Option<NumFormatter<'a>>,
|
custom_formatter: Option<NumFormatter<'a>>,
|
||||||
custom_parser: Option<NumParser<'a>>,
|
custom_parser: Option<NumParser<'a>>,
|
||||||
|
trailing_fill: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Slider<'a> {
|
impl<'a> Slider<'a> {
|
||||||
|
@ -129,6 +130,7 @@ impl<'a> Slider<'a> {
|
||||||
max_decimals: None,
|
max_decimals: None,
|
||||||
custom_formatter: None,
|
custom_formatter: None,
|
||||||
custom_parser: None,
|
custom_parser: None,
|
||||||
|
trailing_fill: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +271,17 @@ impl<'a> Slider<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display trailing color behind the slider's circle. Default is OFF.
|
||||||
|
///
|
||||||
|
/// This setting can be enabled globally for all sliders with [`Visuals::slider_trailing_fill`].
|
||||||
|
/// Toggling it here will override the above setting ONLY for this individual slider.
|
||||||
|
///
|
||||||
|
/// The fill color will be taken from `selection.bg_fill` in your [`Visuals`], the same as a [`ProgressBar`].
|
||||||
|
pub fn trailing_fill(mut self, trailing_fill: bool) -> Self {
|
||||||
|
self.trailing_fill = Some(trailing_fill);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set custom formatter defining how numbers are converted into text.
|
/// 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
|
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
|
||||||
|
@ -540,7 +553,7 @@ impl<'a> Slider<'a> {
|
||||||
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
|
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
|
||||||
let position = self.pointer_position(pointer_position_2d);
|
let position = self.pointer_position(pointer_position_2d);
|
||||||
let new_value = if self.smart_aim {
|
let new_value = if self.smart_aim {
|
||||||
let aim_radius = ui.input().aim_radius();
|
let aim_radius = ui.input(|i| i.aim_radius());
|
||||||
emath::smart_aim::best_in_range_f64(
|
emath::smart_aim::best_in_range_f64(
|
||||||
self.value_from_position(position - aim_radius, position_range.clone()),
|
self.value_from_position(position - aim_radius, position_range.clone()),
|
||||||
self.value_from_position(position + aim_radius, position_range.clone()),
|
self.value_from_position(position + aim_radius, position_range.clone()),
|
||||||
|
@ -562,19 +575,19 @@ impl<'a> Slider<'a> {
|
||||||
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
||||||
};
|
};
|
||||||
|
|
||||||
decrement += ui.input().num_presses(dec_key);
|
ui.input(|input| {
|
||||||
increment += ui.input().num_presses(inc_key);
|
decrement += input.num_presses(dec_key);
|
||||||
|
increment += input.num_presses(inc_key);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
use accesskit::Action;
|
use accesskit::Action;
|
||||||
decrement += ui
|
ui.input(|input| {
|
||||||
.input()
|
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||||
.num_accesskit_action_requests(response.id, Action::Decrement);
|
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
|
||||||
increment += ui
|
});
|
||||||
.input()
|
|
||||||
.num_accesskit_action_requests(response.id, Action::Increment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let kb_step = increment as f32 - decrement as f32;
|
let kb_step = increment as f32 - decrement as f32;
|
||||||
|
@ -586,7 +599,7 @@ impl<'a> Slider<'a> {
|
||||||
let new_value = match self.step {
|
let new_value = match self.step {
|
||||||
Some(step) => prev_value + (kb_step as f64 * step),
|
Some(step) => prev_value + (kb_step as f64 * step),
|
||||||
None if self.smart_aim => {
|
None if self.smart_aim => {
|
||||||
let aim_radius = ui.input().aim_radius();
|
let aim_radius = ui.input(|i| i.aim_radius());
|
||||||
emath::smart_aim::best_in_range_f64(
|
emath::smart_aim::best_in_range_f64(
|
||||||
self.value_from_position(new_position - aim_radius, position_range.clone()),
|
self.value_from_position(new_position - aim_radius, position_range.clone()),
|
||||||
self.value_from_position(new_position + aim_radius, position_range.clone()),
|
self.value_from_position(new_position + aim_radius, position_range.clone()),
|
||||||
|
@ -600,14 +613,13 @@ impl<'a> Slider<'a> {
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
use accesskit::{Action, ActionData};
|
use accesskit::{Action, ActionData};
|
||||||
for request in ui
|
ui.input(|input| {
|
||||||
.input()
|
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
|
||||||
.accesskit_action_requests(response.id, Action::SetValue)
|
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||||
{
|
self.set_value(new_value);
|
||||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
}
|
||||||
self.set_value(new_value);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint it:
|
// Paint it:
|
||||||
|
@ -617,22 +629,40 @@ impl<'a> Slider<'a> {
|
||||||
let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect));
|
let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect));
|
||||||
let rail_rect = self.rail_rect(rect, rail_radius);
|
let rail_rect = self.rail_rect(rect, rail_radius);
|
||||||
|
|
||||||
let position_1d = self.position_from_value(value, position_range);
|
|
||||||
|
|
||||||
let visuals = ui.style().interact(response);
|
let visuals = ui.style().interact(response);
|
||||||
ui.painter().add(epaint::RectShape {
|
let widget_visuals = &ui.visuals().widgets;
|
||||||
rect: rail_rect,
|
|
||||||
rounding: ui.visuals().widgets.inactive.rounding,
|
|
||||||
fill: ui.visuals().widgets.inactive.bg_fill,
|
|
||||||
// fill: visuals.bg_fill,
|
|
||||||
// fill: ui.visuals().extreme_bg_color,
|
|
||||||
stroke: Default::default(),
|
|
||||||
// stroke: visuals.bg_stroke,
|
|
||||||
// stroke: ui.visuals().widgets.inactive.bg_stroke,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
rail_rect,
|
||||||
|
widget_visuals.inactive.rounding,
|
||||||
|
widget_visuals.inactive.bg_fill,
|
||||||
|
);
|
||||||
|
|
||||||
|
let position_1d = self.position_from_value(value, position_range);
|
||||||
let center = self.marker_center(position_1d, &rail_rect);
|
let center = self.marker_center(position_1d, &rail_rect);
|
||||||
|
|
||||||
|
// Decide if we should add trailing fill.
|
||||||
|
let trailing_fill = self
|
||||||
|
.trailing_fill
|
||||||
|
.unwrap_or_else(|| ui.visuals().slider_trailing_fill);
|
||||||
|
|
||||||
|
// Paint trailing fill.
|
||||||
|
if trailing_fill {
|
||||||
|
let mut trailing_rail_rect = rail_rect;
|
||||||
|
|
||||||
|
// The trailing rect has to be drawn differently depending on the orientation.
|
||||||
|
match self.orientation {
|
||||||
|
SliderOrientation::Vertical => trailing_rail_rect.min.y = center.y,
|
||||||
|
SliderOrientation::Horizontal => trailing_rail_rect.max.x = center.x,
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
trailing_rail_rect,
|
||||||
|
widget_visuals.inactive.rounding,
|
||||||
|
ui.visuals().selection.bg_fill,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ui.painter().add(epaint::CircleShape {
|
ui.painter().add(epaint::CircleShape {
|
||||||
center,
|
center,
|
||||||
radius: self.handle_radius(rect) + visuals.expansion,
|
radius: self.handle_radius(rect) + visuals.expansion,
|
||||||
|
@ -697,14 +727,12 @@ impl<'a> Slider<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
|
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
|
||||||
let change = {
|
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||||
// Hold one lock rather than 4 (see https://github.com/emilk/egui/pull/1380).
|
let change = ui.input(|input| {
|
||||||
let input = ui.input();
|
|
||||||
|
|
||||||
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
|
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
|
||||||
- input.num_presses(Key::ArrowDown) as i32
|
- input.num_presses(Key::ArrowDown) as i32
|
||||||
- input.num_presses(Key::ArrowLeft) as i32
|
- input.num_presses(Key::ArrowLeft) as i32
|
||||||
};
|
});
|
||||||
|
|
||||||
let any_change = change != 0;
|
let any_change = change != 0;
|
||||||
let speed = if let (Some(step), true) = (self.step, any_change) {
|
let speed = if let (Some(step), true) = (self.step, any_change) {
|
||||||
|
@ -764,20 +792,22 @@ impl<'a> Slider<'a> {
|
||||||
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
||||||
use accesskit::Action;
|
use accesskit::Action;
|
||||||
node.min_numeric_value = Some(*self.range.start());
|
builder.set_min_numeric_value(*self.range.start());
|
||||||
node.max_numeric_value = Some(*self.range.end());
|
builder.set_max_numeric_value(*self.range.end());
|
||||||
node.numeric_value_step = self.step;
|
if let Some(step) = self.step {
|
||||||
node.actions |= Action::SetValue;
|
builder.set_numeric_value_step(step);
|
||||||
|
}
|
||||||
|
builder.add_action(Action::SetValue);
|
||||||
let clamp_range = self.clamp_range();
|
let clamp_range = self.clamp_range();
|
||||||
if value < *clamp_range.end() {
|
if value < *clamp_range.end() {
|
||||||
node.actions |= Action::Increment;
|
builder.add_action(Action::Increment);
|
||||||
}
|
}
|
||||||
if value > *clamp_range.start() {
|
if value > *clamp_range.start() {
|
||||||
node.actions |= Action::Decrement;
|
builder.add_action(Action::Decrement);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
let slider_response = response.clone();
|
let slider_response = response.clone();
|
||||||
|
|
||||||
|
|
|
@ -48,8 +48,9 @@ impl Widget for Spinner {
|
||||||
|
|
||||||
let radius = (rect.height() / 2.0) - 2.0;
|
let radius = (rect.height() / 2.0) - 2.0;
|
||||||
let n_points = 20;
|
let n_points = 20;
|
||||||
let start_angle = ui.input().time * std::f64::consts::TAU;
|
let time = ui.input(|i| i.time);
|
||||||
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
let start_angle = time * std::f64::consts::TAU;
|
||||||
|
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
||||||
let points: Vec<Pos2> = (0..n_points)
|
let points: Vec<Pos2> = (0..n_points)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
||||||
|
|
|
@ -19,7 +19,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
|
||||||
/// if response.changed() {
|
/// if response.changed() {
|
||||||
/// // …
|
/// // …
|
||||||
/// }
|
/// }
|
||||||
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
/// // …
|
/// // …
|
||||||
/// }
|
/// }
|
||||||
/// # });
|
/// # });
|
||||||
|
@ -67,6 +67,9 @@ pub struct TextEdit<'t> {
|
||||||
desired_height_rows: usize,
|
desired_height_rows: usize,
|
||||||
lock_focus: bool,
|
lock_focus: bool,
|
||||||
cursor_at_end: bool,
|
cursor_at_end: bool,
|
||||||
|
min_size: Vec2,
|
||||||
|
align: Align2,
|
||||||
|
clip_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> WidgetWithState for TextEdit<'t> {
|
impl<'t> WidgetWithState for TextEdit<'t> {
|
||||||
|
@ -89,6 +92,7 @@ impl<'t> TextEdit<'t> {
|
||||||
Self {
|
Self {
|
||||||
desired_height_rows: 1,
|
desired_height_rows: 1,
|
||||||
multiline: false,
|
multiline: false,
|
||||||
|
clip_text: true,
|
||||||
..Self::multiline(text)
|
..Self::multiline(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +116,9 @@ impl<'t> TextEdit<'t> {
|
||||||
desired_height_rows: 4,
|
desired_height_rows: 4,
|
||||||
lock_focus: false,
|
lock_focus: false,
|
||||||
cursor_at_end: true,
|
cursor_at_end: true,
|
||||||
|
min_size: Vec2::ZERO,
|
||||||
|
align: Align2::LEFT_TOP,
|
||||||
|
clip_text: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +213,7 @@ impl<'t> TextEdit<'t> {
|
||||||
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
||||||
/// layout_job.wrap.max_width = wrap_width;
|
/// layout_job.wrap.max_width = wrap_width;
|
||||||
/// ui.fonts().layout_job(layout_job)
|
/// ui.fonts(|f| f.layout_job(layout_job))
|
||||||
/// };
|
/// };
|
||||||
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
||||||
/// # });
|
/// # });
|
||||||
|
@ -269,6 +276,37 @@ impl<'t> TextEdit<'t> {
|
||||||
self.cursor_at_end = b;
|
self.cursor_at_end = b;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When `true` (default), overflowing text will be clipped.
|
||||||
|
///
|
||||||
|
/// When `false`, widget width will expand to make all text visible.
|
||||||
|
///
|
||||||
|
/// This only works for singleline [`TextEdit`].
|
||||||
|
pub fn clip_text(mut self, b: bool) -> Self {
|
||||||
|
// always show everything in multiline
|
||||||
|
if !self.multiline {
|
||||||
|
self.clip_text = b;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the horizontal align of the inner text.
|
||||||
|
pub fn horizontal_align(mut self, align: Align) -> Self {
|
||||||
|
self.align.0[0] = align;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the vertical align of the inner text.
|
||||||
|
pub fn vertical_align(mut self, align: Align) -> Self {
|
||||||
|
self.align.0[1] = align;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the minimum size of the [`TextEdit`].
|
||||||
|
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||||
|
self.min_size = min_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -312,7 +350,7 @@ impl<'t> TextEdit<'t> {
|
||||||
output.response |= ui.interact(frame_rect, id, Sense::click());
|
output.response |= ui.interact(frame_rect, id, Sense::click());
|
||||||
}
|
}
|
||||||
if output.response.clicked() && !output.response.lost_focus() {
|
if output.response.clicked() && !output.response.lost_focus() {
|
||||||
ui.memory().request_focus(output.response.id);
|
ui.memory_mut(|mem| mem.request_focus(output.response.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if frame {
|
if frame {
|
||||||
|
@ -364,13 +402,16 @@ impl<'t> TextEdit<'t> {
|
||||||
layouter,
|
layouter,
|
||||||
password,
|
password,
|
||||||
frame: _,
|
frame: _,
|
||||||
margin: _,
|
margin,
|
||||||
multiline,
|
multiline,
|
||||||
interactive,
|
interactive,
|
||||||
desired_width,
|
desired_width,
|
||||||
desired_height_rows,
|
desired_height_rows,
|
||||||
lock_focus,
|
lock_focus,
|
||||||
cursor_at_end,
|
cursor_at_end,
|
||||||
|
min_size,
|
||||||
|
align,
|
||||||
|
clip_text,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
|
@ -381,7 +422,7 @@ impl<'t> TextEdit<'t> {
|
||||||
let prev_text = text.as_str().to_owned();
|
let prev_text = text.as_str().to_owned();
|
||||||
|
|
||||||
let font_id = font_selection.resolve(ui.style());
|
let font_id = font_selection.resolve(ui.style());
|
||||||
let row_height = ui.fonts().row_height(&font_id);
|
let row_height = ui.fonts(|f| f.row_height(&font_id));
|
||||||
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
|
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
|
||||||
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
||||||
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
||||||
|
@ -389,29 +430,31 @@ impl<'t> TextEdit<'t> {
|
||||||
available_width
|
available_width
|
||||||
} else {
|
} else {
|
||||||
desired_width.min(available_width)
|
desired_width.min(available_width)
|
||||||
};
|
} - margin.x * 2.0;
|
||||||
|
|
||||||
let font_id_clone = font_id.clone();
|
let font_id_clone = font_id.clone();
|
||||||
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
||||||
let text = mask_if_password(password, text);
|
let text = mask_if_password(password, text);
|
||||||
ui.fonts().layout_job(if multiline {
|
let layout_job = if multiline {
|
||||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
||||||
} else {
|
} else {
|
||||||
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
||||||
})
|
};
|
||||||
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
||||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||||
|
|
||||||
let mut galley = layouter(ui, text.as_str(), wrap_width);
|
let mut galley = layouter(ui, text.as_str(), wrap_width);
|
||||||
|
|
||||||
let desired_width = if multiline {
|
let desired_width = if clip_text {
|
||||||
galley.size().x.max(wrap_width) // always show everything in multiline
|
wrap_width // visual clipping with scroll in singleline input.
|
||||||
} else {
|
} else {
|
||||||
wrap_width // visual clipping with scroll in singleline input. TODO(emilk): opt-in/out?
|
galley.size().x.max(wrap_width)
|
||||||
};
|
};
|
||||||
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
||||||
let desired_size = vec2(desired_width, galley.size().y.max(desired_height));
|
let desired_size = vec2(desired_width, galley.size().y.max(desired_height))
|
||||||
|
.at_least(min_size - margin * 2.0);
|
||||||
|
|
||||||
let (auto_id, rect) = ui.allocate_space(desired_size);
|
let (auto_id, rect) = ui.allocate_space(desired_size);
|
||||||
|
|
||||||
|
@ -428,8 +471,8 @@ impl<'t> TextEdit<'t> {
|
||||||
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
||||||
// Since currently copying selected text in not supported on `eframe` web,
|
// Since currently copying selected text in not supported on `eframe` web,
|
||||||
// we prioritize touch-scrolling:
|
// we prioritize touch-scrolling:
|
||||||
let any_touches = ui.input().any_touches(); // separate line to avoid double-locking the same mutex
|
let allow_drag_to_select =
|
||||||
let allow_drag_to_select = !any_touches || ui.memory().has_focus(id);
|
ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id));
|
||||||
|
|
||||||
let sense = if interactive {
|
let sense = if interactive {
|
||||||
if allow_drag_to_select {
|
if allow_drag_to_select {
|
||||||
|
@ -447,7 +490,7 @@ impl<'t> TextEdit<'t> {
|
||||||
if interactive {
|
if interactive {
|
||||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||||
if response.hovered() && text.is_mutable() {
|
if response.hovered() && text.is_mutable() {
|
||||||
ui.output().mutable_text_under_cursor = true;
|
ui.output_mut(|o| o.mutable_text_under_cursor = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||||
|
@ -457,7 +500,7 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
if ui.visuals().text_cursor_preview
|
if ui.visuals().text_cursor_preview
|
||||||
&& response.hovered()
|
&& response.hovered()
|
||||||
&& ui.input().pointer.is_moving()
|
&& ui.input(|i| i.pointer.is_moving())
|
||||||
{
|
{
|
||||||
// preview:
|
// preview:
|
||||||
paint_cursor_end(
|
paint_cursor_end(
|
||||||
|
@ -487,9 +530,9 @@ impl<'t> TextEdit<'t> {
|
||||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||||
}));
|
}));
|
||||||
} else if allow_drag_to_select {
|
} else if allow_drag_to_select {
|
||||||
if response.hovered() && ui.input().pointer.any_pressed() {
|
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||||
ui.memory().request_focus(id);
|
ui.memory_mut(|mem| mem.request_focus(id));
|
||||||
if ui.input().modifiers.shift {
|
if ui.input(|i| i.modifiers.shift) {
|
||||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
||||||
cursor_range.primary = cursor_at_pointer;
|
cursor_range.primary = cursor_at_pointer;
|
||||||
state.set_cursor_range(Some(cursor_range));
|
state.set_cursor_range(Some(cursor_range));
|
||||||
|
@ -499,7 +542,8 @@ impl<'t> TextEdit<'t> {
|
||||||
} else {
|
} else {
|
||||||
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
|
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||||
}
|
}
|
||||||
} else if ui.input().pointer.any_down() && response.is_pointer_button_down_on()
|
} else if ui.input(|i| i.pointer.any_down())
|
||||||
|
&& response.is_pointer_button_down_on()
|
||||||
{
|
{
|
||||||
// drag to select text:
|
// drag to select text:
|
||||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
||||||
|
@ -511,14 +555,14 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.hovered() && interactive {
|
if interactive && response.hovered() {
|
||||||
ui.output().cursor_icon = CursorIcon::Text;
|
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cursor_range = None;
|
let mut cursor_range = None;
|
||||||
let prev_cursor_range = state.cursor_range(&galley);
|
let prev_cursor_range = state.cursor_range(&galley);
|
||||||
if ui.memory().has_focus(id) && interactive {
|
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||||
ui.memory().lock_focus(id, lock_focus);
|
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
|
||||||
|
|
||||||
let default_cursor_range = if cursor_at_end {
|
let default_cursor_range = if cursor_at_end {
|
||||||
CursorRange::one(galley.end())
|
CursorRange::one(galley.end())
|
||||||
|
@ -545,11 +589,15 @@ impl<'t> TextEdit<'t> {
|
||||||
cursor_range = Some(new_cursor_range);
|
cursor_range = Some(new_cursor_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text_draw_pos = response.rect.min;
|
let mut text_draw_pos = align
|
||||||
|
.align_size_within_rect(galley.size(), response.rect)
|
||||||
|
.intersect(response.rect) // limit pos to the response rect area
|
||||||
|
.min;
|
||||||
|
let align_offset = response.rect.left() - text_draw_pos.x;
|
||||||
|
|
||||||
// Visual clipping for singleline text editor with text larger than width
|
// Visual clipping for singleline text editor with text larger than width
|
||||||
if !multiline {
|
if clip_text && align_offset == 0.0 {
|
||||||
let cursor_pos = match (cursor_range, ui.memory().has_focus(id)) {
|
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
|
||||||
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
|
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
};
|
};
|
||||||
|
@ -571,6 +619,8 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
state.singleline_offset = offset_x;
|
state.singleline_offset = offset_x;
|
||||||
text_draw_pos -= vec2(offset_x, 0.0);
|
text_draw_pos -= vec2(offset_x, 0.0);
|
||||||
|
} else {
|
||||||
|
state.singleline_offset = align_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
|
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
|
||||||
|
@ -594,7 +644,7 @@ impl<'t> TextEdit<'t> {
|
||||||
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
|
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_focus(id) {
|
if ui.memory(|mem| mem.has_focus(id)) {
|
||||||
if let Some(cursor_range) = state.cursor_range(&galley) {
|
if let Some(cursor_range) = state.cursor_range(&galley) {
|
||||||
// We paint the cursor on top of the text, in case
|
// We paint the cursor on top of the text, in case
|
||||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||||
|
@ -621,9 +671,13 @@ impl<'t> TextEdit<'t> {
|
||||||
// But `winit` and `egui_web` differs in how to set the
|
// But `winit` and `egui_web` differs in how to set the
|
||||||
// position of IME.
|
// position of IME.
|
||||||
if cfg!(target_arch = "wasm32") {
|
if cfg!(target_arch = "wasm32") {
|
||||||
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
|
ui.ctx().output_mut(|o| {
|
||||||
|
o.text_cursor_pos = Some(cursor_pos.left_top());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_bottom());
|
ui.ctx().output_mut(|o| {
|
||||||
|
o.text_cursor_pos = Some(cursor_pos.left_bottom());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -659,89 +713,100 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
if let Some(mut node) = ui.ctx().accesskit_node(response.id) {
|
{
|
||||||
use accesskit::{Role, TextDirection, TextPosition, TextSelection};
|
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| {
|
||||||
|
use accesskit::{TextPosition, TextSelection};
|
||||||
|
|
||||||
let parent_id = response.id;
|
let parent_id = response.id;
|
||||||
|
|
||||||
if let Some(cursor_range) = &cursor_range {
|
if let Some(cursor_range) = &cursor_range {
|
||||||
let anchor = &cursor_range.secondary.rcursor;
|
let anchor = &cursor_range.secondary.rcursor;
|
||||||
let focus = &cursor_range.primary.rcursor;
|
let focus = &cursor_range.primary.rcursor;
|
||||||
node.text_selection = Some(TextSelection {
|
builder.set_text_selection(TextSelection {
|
||||||
anchor: TextPosition {
|
anchor: TextPosition {
|
||||||
node: parent_id.with(anchor.row).accesskit_id(),
|
node: parent_id.with(anchor.row).accesskit_id(),
|
||||||
character_index: anchor.column,
|
character_index: anchor.column,
|
||||||
},
|
},
|
||||||
focus: TextPosition {
|
focus: TextPosition {
|
||||||
node: parent_id.with(focus.row).accesskit_id(),
|
node: parent_id.with(focus.row).accesskit_id(),
|
||||||
character_index: focus.column,
|
character_index: focus.column,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
|
||||||
|
if self.multiline {
|
||||||
|
builder.set_multiline();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_id
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(parent_id) = parent_id {
|
||||||
|
// drop ctx lock before further processing
|
||||||
|
use accesskit::{Role, TextDirection};
|
||||||
|
|
||||||
|
ui.ctx().with_accessibility_parent(parent_id, || {
|
||||||
|
for (i, row) in galley.rows.iter().enumerate() {
|
||||||
|
let id = parent_id.with(i);
|
||||||
|
ui.ctx().accesskit_node_builder(id, |builder| {
|
||||||
|
builder.set_role(Role::InlineTextBox);
|
||||||
|
let rect = row.rect.translate(text_draw_pos.to_vec2());
|
||||||
|
builder.set_bounds(accesskit::Rect {
|
||||||
|
x0: rect.min.x.into(),
|
||||||
|
y0: rect.min.y.into(),
|
||||||
|
x1: rect.max.x.into(),
|
||||||
|
y1: rect.max.y.into(),
|
||||||
|
});
|
||||||
|
builder.set_text_direction(TextDirection::LeftToRight);
|
||||||
|
// TODO(mwcampbell): Set more node fields for the row
|
||||||
|
// once AccessKit adapters expose text formatting info.
|
||||||
|
|
||||||
|
let glyph_count = row.glyphs.len();
|
||||||
|
let mut value = String::new();
|
||||||
|
value.reserve(glyph_count);
|
||||||
|
let mut character_lengths = Vec::<u8>::new();
|
||||||
|
character_lengths.reserve(glyph_count);
|
||||||
|
let mut character_positions = Vec::<f32>::new();
|
||||||
|
character_positions.reserve(glyph_count);
|
||||||
|
let mut character_widths = Vec::<f32>::new();
|
||||||
|
character_widths.reserve(glyph_count);
|
||||||
|
let mut word_lengths = Vec::<u8>::new();
|
||||||
|
let mut was_at_word_end = false;
|
||||||
|
let mut last_word_start = 0usize;
|
||||||
|
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let is_word_char = is_word_char(glyph.chr);
|
||||||
|
if is_word_char && was_at_word_end {
|
||||||
|
word_lengths
|
||||||
|
.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
last_word_start = character_lengths.len();
|
||||||
|
}
|
||||||
|
was_at_word_end = !is_word_char;
|
||||||
|
let old_len = value.len();
|
||||||
|
value.push(glyph.chr);
|
||||||
|
character_lengths.push((value.len() - old_len) as _);
|
||||||
|
character_positions.push(glyph.pos.x - row.rect.min.x);
|
||||||
|
character_widths.push(glyph.size.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.ends_with_newline {
|
||||||
|
value.push('\n');
|
||||||
|
character_lengths.push(1);
|
||||||
|
character_positions.push(row.rect.max.x - row.rect.min.x);
|
||||||
|
character_widths.push(0.0);
|
||||||
|
}
|
||||||
|
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
|
||||||
|
builder.set_value(value);
|
||||||
|
builder.set_character_lengths(character_lengths);
|
||||||
|
builder.set_character_positions(character_positions);
|
||||||
|
builder.set_character_widths(character_widths);
|
||||||
|
builder.set_word_lengths(word_lengths);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
|
|
||||||
node.multiline = self.multiline;
|
|
||||||
|
|
||||||
drop(node);
|
|
||||||
|
|
||||||
ui.ctx().with_accessibility_parent(parent_id, || {
|
|
||||||
for (i, row) in galley.rows.iter().enumerate() {
|
|
||||||
let id = parent_id.with(i);
|
|
||||||
let mut node = ui.ctx().accesskit_node(id).unwrap();
|
|
||||||
node.role = Role::InlineTextBox;
|
|
||||||
let rect = row.rect.translate(text_draw_pos.to_vec2());
|
|
||||||
node.bounds = Some(accesskit::kurbo::Rect {
|
|
||||||
x0: rect.min.x.into(),
|
|
||||||
y0: rect.min.y.into(),
|
|
||||||
x1: rect.max.x.into(),
|
|
||||||
y1: rect.max.y.into(),
|
|
||||||
});
|
|
||||||
node.text_direction = Some(TextDirection::LeftToRight);
|
|
||||||
// TODO(mwcampbell): Set more node fields for the row
|
|
||||||
// once AccessKit adapters expose text formatting info.
|
|
||||||
|
|
||||||
let glyph_count = row.glyphs.len();
|
|
||||||
let mut value = String::new();
|
|
||||||
value.reserve(glyph_count);
|
|
||||||
let mut character_lengths = Vec::<u8>::new();
|
|
||||||
character_lengths.reserve(glyph_count);
|
|
||||||
let mut character_positions = Vec::<f32>::new();
|
|
||||||
character_positions.reserve(glyph_count);
|
|
||||||
let mut character_widths = Vec::<f32>::new();
|
|
||||||
character_widths.reserve(glyph_count);
|
|
||||||
let mut word_lengths = Vec::<u8>::new();
|
|
||||||
let mut was_at_word_end = false;
|
|
||||||
let mut last_word_start = 0usize;
|
|
||||||
|
|
||||||
for glyph in &row.glyphs {
|
|
||||||
let is_word_char = is_word_char(glyph.chr);
|
|
||||||
if is_word_char && was_at_word_end {
|
|
||||||
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
|
||||||
last_word_start = character_lengths.len();
|
|
||||||
}
|
|
||||||
was_at_word_end = !is_word_char;
|
|
||||||
let old_len = value.len();
|
|
||||||
value.push(glyph.chr);
|
|
||||||
character_lengths.push((value.len() - old_len) as _);
|
|
||||||
character_positions.push(glyph.pos.x - row.rect.min.x);
|
|
||||||
character_widths.push(glyph.size.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
if row.ends_with_newline {
|
|
||||||
value.push('\n');
|
|
||||||
character_lengths.push(1);
|
|
||||||
character_positions.push(row.rect.max.x - row.rect.min.x);
|
|
||||||
character_widths.push(0.0);
|
|
||||||
}
|
|
||||||
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
|
||||||
|
|
||||||
node.value = Some(value.into());
|
|
||||||
node.character_lengths = character_lengths.into();
|
|
||||||
node.character_positions = Some(character_positions.into());
|
|
||||||
node.character_widths = Some(character_widths.into());
|
|
||||||
node.word_lengths = word_lengths.into();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
|
@ -812,19 +877,19 @@ fn events(
|
||||||
// We feed state to the undoer both before and after handling input
|
// We feed state to the undoer both before and after handling input
|
||||||
// so that the undoer creates automatic saves even when there are no events for a while.
|
// so that the undoer creates automatic saves even when there are no events for a while.
|
||||||
state.undoer.lock().feed_state(
|
state.undoer.lock().feed_state(
|
||||||
ui.input().time,
|
ui.input(|i| i.time),
|
||||||
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let copy_if_not_password = |ui: &Ui, text: String| {
|
let copy_if_not_password = |ui: &Ui, text: String| {
|
||||||
if !password {
|
if !password {
|
||||||
ui.ctx().output().copied_text = text;
|
ui.ctx().output_mut(|o| o.copied_text = text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut any_change = false;
|
let mut any_change = false;
|
||||||
|
|
||||||
let events = ui.input().events.clone(); // avoid dead-lock by cloning. TODO(emilk): optimize
|
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
|
||||||
for event in &events {
|
for event in &events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy => {
|
Event::Copy => {
|
||||||
|
@ -869,7 +934,7 @@ fn events(
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if multiline && ui.memory().has_lock_focus(id) {
|
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
|
||||||
let mut ccursor = delete_selected(text, &cursor_range);
|
let mut ccursor = delete_selected(text, &cursor_range);
|
||||||
if modifiers.shift {
|
if modifiers.shift {
|
||||||
// TODO(emilk): support removing indentation over a selection?
|
// TODO(emilk): support removing indentation over a selection?
|
||||||
|
@ -893,7 +958,7 @@ fn events(
|
||||||
// TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
|
// TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
|
||||||
Some(CCursorRange::one(ccursor))
|
Some(CCursorRange::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
ui.memory().surrender_focus(id); // End input with enter
|
ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -997,7 +1062,7 @@ fn events(
|
||||||
state.set_cursor_range(Some(cursor_range));
|
state.set_cursor_range(Some(cursor_range));
|
||||||
|
|
||||||
state.undoer.lock().feed_state(
|
state.undoer.lock().feed_state(
|
||||||
ui.input().time,
|
ui.input(|i| i.time),
|
||||||
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,11 @@ pub struct TextEditState {
|
||||||
|
|
||||||
impl TextEditState {
|
impl TextEditState {
|
||||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||||
ctx.data().get_persisted(id)
|
ctx.data_mut(|d| d.get_persisted(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(self, ctx: &Context, id: Id) {
|
pub fn store(self, ctx: &Context, id: Id) {
|
||||||
ctx.data().insert_persisted(id, self);
|
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The the currently selected range of characters.
|
/// The the currently selected range of characters.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui_demo_app"
|
name = "egui_demo_app"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -20,7 +20,7 @@ default = ["glow", "persistence"]
|
||||||
|
|
||||||
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
||||||
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
||||||
screen_reader = ["eframe/screen_reader"] # experimental
|
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
||||||
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
||||||
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
||||||
|
|
||||||
|
@ -30,11 +30,11 @@ wgpu = ["eframe/wgpu", "bytemuck"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
|
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
|
||||||
eframe = { version = "0.20.0", path = "../eframe", default-features = false }
|
eframe = { version = "0.21.0", path = "../eframe", default-features = false }
|
||||||
egui = { version = "0.20.0", path = "../egui", features = [
|
egui = { version = "0.21.0", path = "../egui", features = [
|
||||||
"extra_debug_asserts",
|
"extra_debug_asserts",
|
||||||
] }
|
] }
|
||||||
egui_demo_lib = { version = "0.20.0", path = "../egui_demo_lib", features = [
|
egui_demo_lib = { version = "0.21.0", path = "../egui_demo_lib", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
] }
|
] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
@ -42,7 +42,7 @@ tracing = "0.1"
|
||||||
# Optional dependencies:
|
# Optional dependencies:
|
||||||
|
|
||||||
bytemuck = { version = "1.7.1", optional = true }
|
bytemuck = { version = "1.7.1", optional = true }
|
||||||
egui_extras = { version = "0.20.0", optional = true, path = "../egui_extras" }
|
egui_extras = { version = "0.21.0", optional = true, path = "../egui_extras" }
|
||||||
|
|
||||||
# feature "http":
|
# feature "http":
|
||||||
ehttp = { version = "0.2.0", optional = true }
|
ehttp = { version = "0.2.0", optional = true }
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::{num::NonZeroU64, sync::Arc};
|
use std::{num::NonZeroU64, sync::Arc};
|
||||||
|
|
||||||
use eframe::{
|
use eframe::{
|
||||||
|
egui_wgpu::wgpu::util::DeviceExt,
|
||||||
egui_wgpu::{self, wgpu},
|
egui_wgpu::{self, wgpu},
|
||||||
wgpu::util::DeviceExt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Custom3d {
|
pub struct Custom3d {
|
||||||
|
|
|
@ -35,7 +35,7 @@ impl Default for FractalClock {
|
||||||
impl FractalClock {
|
impl FractalClock {
|
||||||
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||||
if !self.paused {
|
if !self.paused {
|
||||||
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
|
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time));
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
|
||||||
trigger_fetch = true;
|
trigger_fetch = true;
|
||||||
}
|
}
|
||||||
if ui.button("Random image").clicked() {
|
if ui.button("Random image").clicked() {
|
||||||
let seed = ui.input().time;
|
let seed = ui.input(|i| i.time);
|
||||||
let side = 640;
|
let side = 640;
|
||||||
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
|
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
|
||||||
trigger_fetch = true;
|
trigger_fetch = true;
|
||||||
|
@ -187,7 +187,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||||
if let Some(text) = &text {
|
if let Some(text) = &text {
|
||||||
let tooltip = "Click to copy the response body";
|
let tooltip = "Click to copy the response body";
|
||||||
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
||||||
ui.output().copied_text = text.clone();
|
ui.output_mut(|o| o.copied_text = text.clone());
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
}
|
}
|
||||||
|
@ -245,7 +245,7 @@ impl ColoredText {
|
||||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
||||||
let mut layout_job = self.0.clone();
|
let mut layout_job = self.0.clone();
|
||||||
layout_job.wrap.max_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut text = self.0.text.as_str();
|
let mut text = self.0.text.as_str();
|
||||||
|
@ -258,7 +258,7 @@ impl ColoredText {
|
||||||
} else {
|
} else {
|
||||||
let mut job = self.0.clone();
|
let mut job = self.0.clone();
|
||||||
job.wrap.max_width = ui.available_width();
|
job.wrap.max_width = ui.available_width();
|
||||||
let galley = ui.fonts().layout_job(job);
|
let galley = ui.fonts(|f| f.layout_job(job));
|
||||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ impl Default for BackendPanel {
|
||||||
impl BackendPanel {
|
impl BackendPanel {
|
||||||
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
self.frame_history
|
self.frame_history
|
||||||
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
|
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||||
|
|
||||||
match self.run_mode {
|
match self.run_mode {
|
||||||
RunMode::Continuous => {
|
RunMode::Continuous => {
|
||||||
|
@ -130,10 +130,12 @@ impl BackendPanel {
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[cfg(feature = "web_screen-reader")]
|
||||||
{
|
{
|
||||||
let mut screen_reader = ui.ctx().options().screen_reader;
|
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
|
||||||
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
|
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
|
||||||
ui.ctx().options().screen_reader = screen_reader;
|
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
@ -339,9 +341,11 @@ impl EguiWindows {
|
||||||
output_event_history,
|
output_event_history,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
for event in &ctx.output().events {
|
ctx.output(|o| {
|
||||||
output_event_history.push_back(event.clone());
|
for event in &o.events {
|
||||||
}
|
output_event_history.push_back(event.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
while output_event_history.len() > 1000 {
|
while output_event_history.len() > 1000 {
|
||||||
output_event_history.pop_front();
|
output_event_history.pop_front();
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,19 +95,21 @@ impl FrameHistory {
|
||||||
));
|
));
|
||||||
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
|
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
|
||||||
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
||||||
shapes.push(Shape::text(
|
shapes.push(ui.fonts(|f| {
|
||||||
&ui.fonts(),
|
Shape::text(
|
||||||
pos2(rect.left(), y),
|
f,
|
||||||
egui::Align2::LEFT_BOTTOM,
|
pos2(rect.left(), y),
|
||||||
text,
|
egui::Align2::LEFT_BOTTOM,
|
||||||
TextStyle::Monospace.resolve(ui.style()),
|
text,
|
||||||
color,
|
TextStyle::Monospace.resolve(ui.style()),
|
||||||
));
|
color,
|
||||||
|
)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let circle_color = color;
|
let circle_color = color;
|
||||||
let radius = 2.0;
|
let radius = 2.0;
|
||||||
let right_side_time = ui.input().time; // Time at right side of screen
|
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
|
||||||
|
|
||||||
for (time, cpu_usage) in history.iter() {
|
for (time, cpu_usage) in history.iter() {
|
||||||
let age = (right_side_time - time) as f32;
|
let age = (right_side_time - time) as f32;
|
||||||
|
|
|
@ -175,8 +175,8 @@ impl eframe::App for WrapApp {
|
||||||
eframe::set_value(storage, eframe::APP_KEY, &self.state);
|
eframe::set_value(storage, eframe::APP_KEY, &self.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
|
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
|
||||||
visuals.panel_fill.into()
|
visuals.panel_fill.to_normalized_gamma_f32()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
|
@ -191,10 +191,7 @@ impl eframe::App for WrapApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ctx
|
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) {
|
||||||
.input_mut()
|
|
||||||
.consume_key(egui::Modifiers::NONE, egui::Key::F11)
|
|
||||||
{
|
|
||||||
frame.set_fullscreen(!frame.info().window_info.fullscreen);
|
frame.set_fullscreen(!frame.info().window_info.fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +238,8 @@ impl WrapApp {
|
||||||
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||||
// The backend-panel can be toggled on/off.
|
// The backend-panel can be toggled on/off.
|
||||||
// We show a little animation when the user switches it.
|
// We show a little animation when the user switches it.
|
||||||
let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible();
|
let is_open =
|
||||||
|
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
|
||||||
|
|
||||||
egui::SidePanel::left("backend_panel")
|
egui::SidePanel::left("backend_panel")
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
|
@ -266,13 +264,13 @@ impl WrapApp {
|
||||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
*ui.ctx().memory() = Default::default();
|
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Reset everything").clicked() {
|
if ui.button("Reset everything").clicked() {
|
||||||
self.state = Default::default();
|
self.state = Default::default();
|
||||||
*ui.ctx().memory() = Default::default();
|
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -282,7 +280,7 @@ impl WrapApp {
|
||||||
let mut found_anchor = false;
|
let mut found_anchor = false;
|
||||||
let selected_anchor = self.state.selected_anchor.clone();
|
let selected_anchor = self.state.selected_anchor.clone();
|
||||||
for (_name, anchor, app) in self.apps_iter_mut() {
|
for (_name, anchor, app) in self.apps_iter_mut() {
|
||||||
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
|
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
|
||||||
app.update(ctx, frame);
|
app.update(ctx, frame);
|
||||||
found_anchor = true;
|
found_anchor = true;
|
||||||
}
|
}
|
||||||
|
@ -316,7 +314,7 @@ impl WrapApp {
|
||||||
{
|
{
|
||||||
selected_anchor = anchor.to_owned();
|
selected_anchor = anchor.to_owned();
|
||||||
if frame.is_web() {
|
if frame.is_web() {
|
||||||
ui.output().open_url(format!("#{}", anchor));
|
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,7 +326,7 @@ impl WrapApp {
|
||||||
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
||||||
self.state.selected_anchor = "clock".to_owned();
|
self.state.selected_anchor = "clock".to_owned();
|
||||||
if frame.is_web() {
|
if frame.is_web() {
|
||||||
ui.output().open_url("#clock");
|
ui.output_mut(|o| o.open_url("#clock"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,22 +340,25 @@ impl WrapApp {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
// Preview hovering files:
|
// Preview hovering files:
|
||||||
if !ctx.input().raw.hovered_files.is_empty() {
|
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
|
||||||
let mut text = "Dropping files:\n".to_owned();
|
let text = ctx.input(|i| {
|
||||||
for file in &ctx.input().raw.hovered_files {
|
let mut text = "Dropping files:\n".to_owned();
|
||||||
if let Some(path) = &file.path {
|
for file in &i.raw.hovered_files {
|
||||||
write!(text, "\n{}", path.display()).ok();
|
if let Some(path) = &file.path {
|
||||||
} else if !file.mime.is_empty() {
|
write!(text, "\n{}", path.display()).ok();
|
||||||
write!(text, "\n{}", file.mime).ok();
|
} else if !file.mime.is_empty() {
|
||||||
} else {
|
write!(text, "\n{}", file.mime).ok();
|
||||||
text += "\n???";
|
} else {
|
||||||
|
text += "\n???";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
text
|
||||||
|
});
|
||||||
|
|
||||||
let painter =
|
let painter =
|
||||||
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
|
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
|
||||||
|
|
||||||
let screen_rect = ctx.input().screen_rect();
|
let screen_rect = ctx.screen_rect();
|
||||||
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
|
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
|
||||||
painter.text(
|
painter.text(
|
||||||
screen_rect.center(),
|
screen_rect.center(),
|
||||||
|
@ -369,9 +370,11 @@ impl WrapApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect dropped files:
|
// Collect dropped files:
|
||||||
if !ctx.input().raw.dropped_files.is_empty() {
|
ctx.input(|i| {
|
||||||
self.dropped_files = ctx.input().raw.dropped_files.clone();
|
if !i.raw.dropped_files.is_empty() {
|
||||||
}
|
self.dropped_files = i.raw.dropped_files.clone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show dropped files (if any):
|
// Show dropped files (if any):
|
||||||
if !self.dropped_files.is_empty() {
|
if !self.dropped_files.is_empty() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "egui_demo_lib"
|
name = "egui_demo_lib"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
description = "Example library for egui"
|
description = "Example library for egui"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
@ -30,8 +30,8 @@ syntax_highlighting = ["syntect"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.20.0", path = "../egui", default-features = false }
|
egui = { version = "0.21.0", path = "../egui", default-features = false }
|
||||||
egui_extras = { version = "0.20.0", path = "../egui_extras" }
|
egui_extras = { version = "0.21.0", path = "../egui_extras" }
|
||||||
enum-map = { version = "2", features = ["serde"] }
|
enum-map = { version = "2", features = ["serde"] }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
unicode_names2 = { version = "0.6.0", default-features = false }
|
unicode_names2 = { version = "0.6.0", default-features = false }
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
|
||||||
if false {
|
if false {
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
ctx.memory().set_everything_is_visible(true); // give us everything
|
ctx.memory_mut(|m| m.set_everything_is_visible(true)); // give us everything
|
||||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||||
c.bench_function("demo_full_no_tessellate", |b| {
|
c.bench_function("demo_full_no_tessellate", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
|
|
|
@ -79,7 +79,7 @@ impl super::View for CodeEditor {
|
||||||
let mut layout_job =
|
let mut layout_job =
|
||||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||||
layout_job.wrap.max_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts().layout_job(layout_job)
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
|
|
@ -103,7 +103,7 @@ impl CodeExample {
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||||
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
|
let indentation = 8.0 * ui.fonts(|f| f.glyph_width(&font_id, ' '));
|
||||||
let item_spacing = ui.spacing_mut().item_spacing;
|
let item_spacing = ui.spacing_mut().item_spacing;
|
||||||
ui.add_space(indentation - item_spacing.x);
|
ui.add_space(indentation - item_spacing.x);
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl super::View for DancingStrings {
|
||||||
|
|
||||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
let time = ui.input().time;
|
let time = ui.input(|i| i.time);
|
||||||
|
|
||||||
let desired_size = ui.available_width() * vec2(1.0, 0.35);
|
let desired_size = ui.available_width() * vec2(1.0, 0.35);
|
||||||
let (_id, rect) = ui.allocate_space(desired_size);
|
let (_id, rect) = ui.allocate_space(desired_size);
|
||||||
|
|
|
@ -90,6 +90,7 @@ impl Default for Tests {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::from_demos(vec![
|
Self::from_demos(vec![
|
||||||
Box::new(super::tests::CursorTest::default()),
|
Box::new(super::tests::CursorTest::default()),
|
||||||
|
Box::new(super::highlighting::Highlighting::default()),
|
||||||
Box::new(super::tests::IdTest::default()),
|
Box::new(super::tests::IdTest::default()),
|
||||||
Box::new(super::tests::InputTest::default()),
|
Box::new(super::tests::InputTest::default()),
|
||||||
Box::new(super::layout_test::LayoutTest::default()),
|
Box::new(super::layout_test::LayoutTest::default()),
|
||||||
|
@ -177,7 +178,7 @@ impl DemoWindows {
|
||||||
|
|
||||||
fn mobile_ui(&mut self, ctx: &Context) {
|
fn mobile_ui(&mut self, ctx: &Context) {
|
||||||
if self.about_is_open {
|
if self.about_is_open {
|
||||||
let screen_size = ctx.input().screen_rect.size();
|
let screen_size = ctx.input(|i| i.screen_rect.size());
|
||||||
let default_width = (screen_size.x - 20.0).min(400.0);
|
let default_width = (screen_size.x - 20.0).min(400.0);
|
||||||
|
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
|
@ -216,7 +217,7 @@ impl DemoWindows {
|
||||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||||
self.demo_list_ui(ui);
|
self.demo_list_ui(ui);
|
||||||
if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
|
if ui.ui_contains_pointer() && ui.input(|i| i.pointer.any_click()) {
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -291,7 +292,7 @@ impl DemoWindows {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if ui.button("Organize windows").clicked() {
|
if ui.button("Organize windows").clicked() {
|
||||||
ui.ctx().memory().reset_areas();
|
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -309,12 +310,12 @@ fn file_menu_button(ui: &mut Ui) {
|
||||||
// NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu,
|
// NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu,
|
||||||
// or else they would only be checked if the "File" menu was actually open!
|
// or else they would only be checked if the "File" menu was actually open!
|
||||||
|
|
||||||
if ui.input_mut().consume_shortcut(&organize_shortcut) {
|
if ui.input_mut(|i| i.consume_shortcut(&organize_shortcut)) {
|
||||||
ui.ctx().memory().reset_areas();
|
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.input_mut().consume_shortcut(&reset_shortcut) {
|
if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) {
|
||||||
*ui.ctx().memory() = Default::default();
|
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
|
@ -335,7 +336,7 @@ fn file_menu_button(ui: &mut Ui) {
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
ui.ctx().memory().reset_areas();
|
ui.ctx().memory_mut(|mem| mem.reset_areas());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,7 +348,7 @@ fn file_menu_button(ui: &mut Ui) {
|
||||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
*ui.ctx().memory() = Default::default();
|
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use egui::*;
|
use egui::*;
|
||||||
|
|
||||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||||
let is_being_dragged = ui.memory().is_being_dragged(id);
|
let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id));
|
||||||
|
|
||||||
if !is_being_dragged {
|
if !is_being_dragged {
|
||||||
let response = ui.scope(body).response;
|
let response = ui.scope(body).response;
|
||||||
|
@ -9,10 +9,10 @@ pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||||
// Check for drags:
|
// Check for drags:
|
||||||
let response = ui.interact(response.rect, id, Sense::drag());
|
let response = ui.interact(response.rect, id, Sense::drag());
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
ui.output().cursor_icon = CursorIcon::Grab;
|
ui.ctx().set_cursor_icon(CursorIcon::Grab);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.output().cursor_icon = CursorIcon::Grabbing;
|
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
|
||||||
|
|
||||||
// Paint the body to a new layer:
|
// Paint the body to a new layer:
|
||||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||||
|
@ -37,7 +37,7 @@ pub fn drop_target<R>(
|
||||||
can_accept_what_is_being_dragged: bool,
|
can_accept_what_is_being_dragged: bool,
|
||||||
body: impl FnOnce(&mut Ui) -> R,
|
body: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<R> {
|
) -> InnerResponse<R> {
|
||||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
|
||||||
|
|
||||||
let margin = Vec2::splat(4.0);
|
let margin = Vec2::splat(4.0);
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ impl super::View for DragAndDropDemo {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if ui.memory().is_being_dragged(item_id) {
|
if ui.memory(|mem| mem.is_being_dragged(item_id)) {
|
||||||
source_col_row = Some((col_idx, row_idx));
|
source_col_row = Some((col_idx, row_idx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ impl super::View for DragAndDropDemo {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
|
||||||
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||||
drop_col = Some(col_idx);
|
drop_col = Some(col_idx);
|
||||||
}
|
}
|
||||||
|
@ -162,7 +162,7 @@ impl super::View for DragAndDropDemo {
|
||||||
|
|
||||||
if let Some((source_col, source_row)) = source_col_row {
|
if let Some((source_col, source_row)) = source_col_row {
|
||||||
if let Some(drop_col) = drop_col {
|
if let Some(drop_col) = drop_col {
|
||||||
if ui.input().pointer.any_released() {
|
if ui.input(|i| i.pointer.any_released()) {
|
||||||
// do the drop:
|
// do the drop:
|
||||||
let item = self.columns[source_col].remove(source_row);
|
let item = self.columns[source_col].remove(source_row);
|
||||||
self.columns[drop_col].push(item);
|
self.columns[drop_col].push(item);
|
||||||
|
|
|
@ -93,7 +93,7 @@ impl super::View for FontBook {
|
||||||
};
|
};
|
||||||
|
|
||||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
||||||
ui.output().copied_text = chr.to_string();
|
ui.output_mut(|o| o.copied_text = chr.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,15 +103,16 @@ impl super::View for FontBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
|
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
|
||||||
ui.fonts()
|
ui.fonts(|f| {
|
||||||
.lock()
|
f.lock()
|
||||||
.fonts
|
.fonts
|
||||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||||
.characters()
|
.characters()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||||
.map(|&chr| (chr, char_name(chr)))
|
.map(|&chr| (chr, char_name(chr)))
|
||||||
.collect()
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_name(chr: char) -> String {
|
fn char_name(chr: char) -> String {
|
||||||
|
|
37
crates/egui_demo_lib/src/demo/highlighting.rs
Normal file
37
crates/egui_demo_lib/src/demo/highlighting.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -202,7 +202,7 @@ impl Widgets {
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
// Trick so we don't have to add spaces in the text below:
|
// Trick so we don't have to add spaces in the text below:
|
||||||
let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' ');
|
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
|
||||||
ui.spacing_mut().item_spacing.x = width;
|
ui.spacing_mut().item_spacing.x = width;
|
||||||
|
|
||||||
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
|
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub mod dancing_strings;
|
||||||
pub mod demo_app_windows;
|
pub mod demo_app_windows;
|
||||||
pub mod drag_and_drop;
|
pub mod drag_and_drop;
|
||||||
pub mod font_book;
|
pub mod font_book;
|
||||||
|
pub mod highlighting;
|
||||||
pub mod layout_test;
|
pub mod layout_test;
|
||||||
pub mod misc_demo_window;
|
pub mod misc_demo_window;
|
||||||
pub mod multi_touch;
|
pub mod multi_touch;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue